使用特定的 HttpMessageHandler 注入单实例 HttpClient

Posted

技术标签:

【中文标题】使用特定的 HttpMessageHandler 注入单实例 HttpClient【英文标题】:Injecting Single Instance HttpClient with specific HttpMessageHandler 【发布时间】:2017-03-30 13:09:37 【问题描述】:

作为我正在处理的 ASP.Net Core 项目的一部分,我需要从我的 WebApi 中与许多不同的基于 Rest 的 API 端点进行通信。为了实现这一点,我使用了许多服务类,每个服务类都实例化一个静态HttpClient。本质上,我为 WebApi 连接到的每个基于 Rest 的端点都有一个服务类。

下面是如何在每个服务类中实例化静态HttpClient 的示例。

private static HttpClient _client = new HttpClient()

    BaseAddress = new Uri("http://endpointurlexample"),            
;

虽然上述方法运行良好,但它不允许对使用HttpClient 的服务类进行有效的单元测试。为了使我能够进行单元测试,我有一个假的HttpMessageHandler,我想在我的单元测试中将它用于HttpClient,而HttpClient 的实例化如上,但是我无法应用假的@987654328 @ 作为我的单元测试的一部分。

服务类中的HttpClient 在整个应用程序中保持单个实例(每个端点一个实例)但允许在单元测试期间应用不同的HttpMessageHandler 的最佳方式是什么?

我想到的一种方法是不在服务类中使用静态字段来保存HttpClient,而是允许使用单例生命周期通过构造函数注入来注入它,这将允许我指定一个HttpClient 在单元测试期间使用所需的HttpMessageHandler,我想到的另一个选项是使用HttpClient 工厂类,它在静态字段中实例化HttpClients,然后可以通过注入HttpClient 来检索工厂进入服务类,再次允许在单元测试中返回具有相关HttpMessageHandler 的不同实现。然而,以上都没有感觉特别干净,感觉必须有更好的方法?

有任何问题,请告诉我。

【问题讨论】:

如果它是通过控制器使用的,那么控制器如何在构造函数中使用 httpClient 并使用 Moq 进行单元测试? 它是通过控制器使用的,但不是将 HttpClient 注入控制器,而是注入一个服务,其中一个实现使用 HttpClient。将来可能需要使用 Http 以外的机制的其他实现,这就是为什么不直接将 HttpClient 提供给控制器的原因。 【参考方案1】:

添加到 cmets 的对话看起来您需要一个 HttpClient 工厂

public interface IHttpClientFactory 
    HttpClient Create(string endpoint);

核心功能的实现可能看起来像这样。

public class DefaultHttpClientFactory : IHttpClientFactory, IDisposable

    private readonly ConcurrentDictionary<string, HttpClient> _httpClients;

    public DefaultHttpClientFactory()
    
        this._httpClients = new ConcurrentDictionary<string, HttpClient>();
    

    public HttpClient Create(string endpoint)
    
        if (this._httpClients.TryGetValue(endpoint, out var client))
        
            return client;
        

        client = new HttpClient
        
            BaseAddress = new Uri(endpoint),
        ;

        this._httpClients.TryAdd(endpoint, client);

        return client;
    

    public void Dispose()
    
        this.Dispose(true);
        GC.SuppressFinalize(this);
    

    protected virtual void Dispose(bool disposing)
    
        foreach (var httpClient in this._httpClients)
        
            httpClient.Value.Dispose();
        
    

也就是说,如果您对上述设计不是特别满意。您可以抽象出服务背后的HttpClient 依赖项,这样客户端就不会成为实现细节。

服务的消费者不需要确切知道如何检索数据。

【讨论】:

【参考方案2】:

你想的很复杂。您只需要一个带有HttpClient 属性的HttpClient 工厂或访问器,并以与ASP.NET Core 允许注入HttpContext 相同的方式使用它

public interface IHttpClientAccessor 

    HttpClient Client  get; 


public class DefaultHttpClientAccessor : IHttpClientAccessor

    public HttpClient Client  get; 

    public DefaultHttpClientAccessor()
    
        Client = new HttpClient();
    

并将其注入您的服务中

public class MyRestClient : IRestClient

    private readonly HttpClient client;

    public MyRestClient(IHttpClientAccessor httpClientAccessor)
    
        client = httpClientAccessor.Client;
    

在 Startup.cs 中注册:

services.AddSingleton<IHttpClientAccessor, DefaultHttpClientAccessor>();

对于单元测试,只需模拟它

// Moq-esque

// Arrange
var httpClientAccessor = new Mock<IHttpClientAccessor>();
var httpHandler = new HttpMessageHandler(..)  ... ;
var httpContext = new HttpContext(httpHandler);

httpClientAccessor.SetupGet(a => a.Client).Returns(httpContext);

// Act
var restClient = new MyRestClient(httpClientAccessor.Object);
var result = await restClient.GetSomethingAsync(...);

// Assert
...

【讨论】:

我喜欢这里的方法,但是这将为我提供一个用于访问所有 3 个端点的 HttpClient。出于多种原因,我希望每个端点都有一个单独的 HttpClient 。因此,Endpoint1 将被 HttpClient instance1 访问,Endpoint2 将被 HttpClient instance2 访问。也许可以使用相同的方法,将 HttpClients 存储在 HttpClientAccessor 内的字典中?使用允许通过基于字符串的键进行检索的方法。想法? 好吧,那么您需要一个带有 Create(string endpoint) 方法的工厂,并为每个端点创建一个 HttpContext 并缓存它,因此您可以在对 Create 方法的最终调用中返回它并将端点用作字典键 @ChrisLawrence 我赞同 Tseng 的最后一个建议。我也是这样处理它的。 @NaveedAhmed:只读属性与只读字段不同。 只读属性HttpContext Client get; ;只读字段:private readonly HttpContext client;。字段没有 getter/setter 这与 IHttpClientFactory 有何不同?这在当时是不可用的,还是完全不同的想法?【参考方案3】:

我目前的偏好是每个目标端点域从HttpClient 派生一次,并使用依赖注入使其成为单例,而不是直接使用HttpClient

假设我正在向 example.com 发出 HTTP 请求,我将拥有一个继承自 HttpClientExampleHttpClient,并且具有与 HttpClient 相同的构造函数签名,允许您像往常一样传递和模拟 HttpMessageHandler

public class ExampleHttpClient : HttpClient

   public ExampleHttpClient(HttpMessageHandler handler) : base(handler) 
   
       BaseAddress = new Uri("http://example.com");

       // set default headers here: content type, authentication, etc   
   

然后我在我的依赖注入注册中将ExampleHttpClient 设置为单例,并将HttpMessageHandler 的注册添加为瞬态,因为每个http 客户端类型只会创建一次。使用这种模式,我不需要为HttpClient 或智能工厂进行多次复杂的注册,以根据目标主机名构建它们。

任何需要与 example.com 通信的东西都应该在 ExampleHttpClient 上建立一个构造函数依赖关系,然后它们都共享同一个实例,您就可以获得设计的连接池。

这种方式还为您提供了一个更好的位置来放置默认标头、内容类型、授权、基地址等内容,并有助于防止一个服务的 http 配置泄漏到另一个服务。

【讨论】:

我喜欢这个主意。我通常只访问 2-3 个不同的端点,所以这可以很好地工作。如果您有许多端点或更复杂的场景,我不禁想知道这里的解决方案是什么。【参考方案4】:

我可能迟到了,但我创建了一个 Helper nuget 包,允许您在单元测试中测试 HttpClient 端点。

NuGet:install-package WorldDomination.HttpClient.Helpers 回购:https://github.com/PureKrome/HttpClient.Helpers

基本思想是创建假响应负载并将FakeHttpMessageHandler 实例传递给您的代码,其中包括该假响应负载。然后,当您的代码尝试实际命中该 URI 端点时,它不会……而只是返回虚假响应。魔法!

这是一个非常简单的例子:

[Fact]
public async Task GivenSomeValidHttpRequests_GetSomeDataAsync_ReturnsAFoo()

    // Arrange.

    // Fake response.
    const string responseData = " \"Id\":69, \"Name\":\"Jane\" ";
    var messageResponse = FakeHttpMessageHandler.GetStringHttpResponseMessage(responseData);

    // Prepare our 'options' with all of the above fake stuff.
    var options = new HttpMessageOptions
    
        RequestUri = MyService.GetFooEndPoint,
        HttpResponseMessage = messageResponse
    ;

    // 3. Use the fake response if that url is attempted.
    var messageHandler = new FakeHttpMessageHandler(options);

    var myService = new MyService(messageHandler);

    // Act.
    // NOTE: network traffic will not leave your computer because you've faked the response, above.
    var result = await myService.GetSomeFooDataAsync();

    // Assert.
    result.Id.ShouldBe(69); // Returned from GetSomeFooDataAsync.
    result.Baa.ShouldBeNull();
    options.NumberOfTimesCalled.ShouldBe(1);

【讨论】:

【参考方案5】:

里面使用了HttpClient:

public class CustomAuthorizationAttribute : Attribute, IAuthorizationFilter

    private string Roles;
    private static readonly HttpClient _httpClient = new HttpClient();

【讨论】:

以上是关于使用特定的 HttpMessageHandler 注入单实例 HttpClient的主要内容,如果未能解决你的问题,请参考以下文章

初始化后将 HttpMessageHandler 设置为 HttpClient 的实例

干货.NET WebApi HttpMessageHandler管道

从配置文件配置 HttpClient/HttpMessageHandler

通过 StructureMap 定义“HttpClient”单例会导致有关“HttpMessageHandler”未在运行时配置的错误

是单例 HttpClient X 分钟后接收新的 HttpMessageHandler

Web API框架学习——消息管道