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

Injecting Single Instance HttpClient with specific HttpMessageHandler

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

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

private static HttpClient _client = new HttpClient()
{
    BaseAddress = new Uri("http://endpointurlexample"),            
};

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

服务 classes 中的 HttpClient 在整个应用程序中保持单个实例(每个端点一个实例)但允许不同的 HttpMessageHandler 在单元测试期间应用?

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

有任何问题,请告诉我。

你想的太复杂了。您所需要的只是一个带有 HttpClient 属性 的 HttpClient 工厂或访问器,并按照 ASP.NET 核心允许 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
...

从评论添加到对话看起来你需要一个 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 依赖性,这样客户端就不会成为实现细节。

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

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

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

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

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

[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);
}

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

假设我正在向 example.com 发出 HTTP 请求,我将有一个 ExampleHttpClient 继承自 HttpClient 并且具有与 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 的构造函数,然后它们都共享同一个实例,你就可以按照设计获得连接池。

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

内部使用了HttpClient:

public class CustomAuthorizationAttribute : Attribute, IAuthorizationFilter
{
    private string Roles;
    private static readonly HttpClient _httpClient = new HttpClient();