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

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

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

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

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

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

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

有任何问题,请告诉我。

从评论添加到对话看起来你需要一个HttpClient工厂

 public interface IHttpClientFactory { HttpClient Create(string endpoint); } 

并且核心function的实现看起来像这样。

 static IDictionary cache = new Dictionary(); public HttpClient Create(string endpoint) { HttpClient client = null; if(cache.TryGetValue(endpoint, out client)) { return client; } client = new HttpClient { BaseAddress = new Uri(endpoint), }; cache[endpoint] = client; return client; } 

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

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

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

 public IHttpClientAccessor { HttpClient HttpClient { get; } } public 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.HttpClient; } } 

在Startup.cs中注册:

 services.AddSingleton(); 

对于unit testing,只需嘲笑它

 // Moq-esque // Arrange var httpClientAccessor = new Mock(); var httpHandler = new HttpMessageHandler(..) { ... }; var httpContext = new HttpContext(httpHandler); httpClientAccessor.SetupGet(a => a.HttpClient).Returns(httpContext); // Act var restClient = new MyRestClient(httpClientAccessor.Object); var result = await restClient.GetSomethingAsync(...); // Assert ... 

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

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

基本思想是创建伪响应有效负载并将FakeHttpMessageHandler实例传递给您的代码,其中包括伪响应有效负载。 然后,当您的代码尝试实际HIT该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); } 

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

假设我正在向example.com发出HTTP请求,我会有一个inheritance自HttpClientExampleHttpClient ,并且具有与HttpClient相同的构造签名,允许您像HttpMessageHandler一样传递和模拟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 } } 

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

任何需要与example.com交谈的东西都应该对ExampleHttpClient采用构造函数依赖,然后它们共享同一个实例,并按设计获得连接池。

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

内部使用HttpClient:

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