使用特定的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自HttpClient
的ExampleHttpClient
,并且具有与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();