使用特定的 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
工厂类,它在静态字段中实例化HttpClient
s,然后可以通过注入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 请求,我将拥有一个继承自 HttpClient
的 ExampleHttpClient
,并且具有与 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”未在运行时配置的错误