使用特定的 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 来检索静态字段中的 HttpClient
s,再次允许返回具有相关 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();
作为我正在从事的 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 来检索静态字段中的 HttpClient
s,再次允许返回具有相关 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();