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

时间:2017-03-30 13:09:37

标签: c# asp.net-core asp.net-core-mvc .net-core dotnet-httpclient

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

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

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

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

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

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

有任何问题,请告诉我。

5 个答案:

答案 0 :(得分:16)

从评论中添加对话似乎需要.span-legend-item { background-color: #666; color: #fff; } 工厂

HttpClient

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

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

那就是说,如果你对上述设计不是特别满意的话。您可以抽象出服务背后的static IDictionary<string, HttpClient> cache = new Dictionary<string, HttpClient>(); 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; } 依赖关系,以便客户端不会成为实现细节。

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

答案 1 :(得分:9)

你觉得很复杂。您只需要一个具有$post_id = array_map(function($item){ return $item->{'post_id'}; },$post_id); 属性的HttpClient工厂或访问器,并以与ASP.NET Core允许HttpClient注入相同的方式使用它

HttpContext

并将其注入您的服务

public IHttpClientAccessor 
{
    HttpClient HttpClient { get; }
}

public DefaultHttpClientAccessor : IHttpClientAccessor
{
    public HttpClient Client { get; }

    public DefaultHttpClientAccessor()
    {
        Client = new HttpClient();
    }
}

在Startup.cs中注册:

public class MyRestClient : IRestClient
{
    private readonly HttpClient client;

    public MyRestClient(IHttpClientAccessor httpClientAccessor)
    {
        client = httpClientAccessor.HttpClient;
    }
}

对于单元测试,只需模拟它

services.AddSingleton<IHttpClientAccessor, DefaultHttpClientAccessor>();

答案 2 :(得分:3)

我目前的偏好是从每个目标端点域一次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的构造函数依赖,然后它们共享同一个实例,并按设计获得连接池。

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

答案 3 :(得分:1)

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

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

基本思想是创建虚假的响应有效负载,并将a FakeHttpMessageHandler实例传递给您的代码,其中包含伪响应有效负载。然后,当您的代码尝试实际HIT该URI端点时,它不会...而只是返回虚假响应。 MAGIC!

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

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

答案 4 :(得分:-1)

内部使用HttpClient:

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