Внедрение единственного экземпляра HttpClient с определенным HttpMessageHandler

В рамках проекта ASP.Net Core, над которым я работаю, у меня есть требование обмениваться данными с рядом различных конечных точек API на основе отдыха из моего WebApi. Для достижения этого я использую ряд классов обслуживания, каждый из которых создает статический HttpClient, По сути, у меня есть класс обслуживания для каждой конечной точки на основе отдыха, к которой подключается WebApi.

Пример того, как статический HttpClient В каждом из классов обслуживания можно увидеть ниже.

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

Хотя вышеприведенное работает хорошо, оно не позволяет проводить эффективное модульное тестирование классов обслуживания, которые используют HttpClient, Чтобы я мог проводить модульное тестирование, у меня есть подделка HttpMessageHandler что я хотел бы использовать для HttpClient в моих модульных тестах, в то время как HttpClient создается как указано выше, однако я не могу применить подделку HttpMessageHandler как часть моих юнит-тестов.

Каков наилучший способ для HttpClient в классах обслуживания, чтобы оставаться единым экземпляром во всем приложении (один экземпляр на конечную точку), но чтобы позволить другой HttpMessageHandler применяться во время юнит-тестов?

Один из подходов, о котором я подумал, - не использовать статическое поле для хранения HttpClient в сервисных классах, а не для того, чтобы это можно было внедрить через конструктор с использованием одноэлементного жизненного цикла, что позволило бы мне указать HttpClient с желаемым HttpMessageHandler во время модульных тестов другой вариант, о котором я подумал, будет использовать HttpClient Фабричный класс, который создал HttpClientв статических полях, которые затем могут быть получены путем введения HttpClient Фабрика в классы обслуживания, снова позволяя другую реализацию с соответствующими HttpMessageHandler быть возвращенным в модульных тестах. Ничто из вышеперечисленного не кажется особенно чистым, и кажется, что должен быть лучший способ?

Любые вопросы, дайте мне знать.

5 ответов

Решение

Добавление к разговору из комментариев выглядит так, как будто вам понадобится 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;
}

Тем не менее, если вы не особенно довольны вышеуказанным дизайном. Вы могли бы абстрагироваться от HttpClient зависимость от службы, так что клиент не становится деталью реализации.

Потребителям услуги не нужно точно знать, как извлекаются данные.

Вы думаете, что сложно. Все, что вам нужно, это фабрика HttpClient или аксессор с HttpClient свойство и использовать его так же, как позволяет ядро ​​ASP.NET 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<IHttpClientAccessor, DefaultHttpClientAccessor>();

Для юнит-тестирования просто смейся

// Moq-esque

// Arrange
var httpClientAccessor = new Mock<IHttpClientAccessor>();
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
...

В настоящее время я предпочитаю HttpClientодин раз для целевого домена конечной точки и сделать его одиночным, используя внедрение зависимостей, а не использование HttpClient непосредственно.

Скажем, я делаю HTTP-запросы к example.com, я бы 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 для одной службы в другую службу.

Возможно, я опоздал на вечеринку, но я создал пакет nuget Helper, который позволяет вам тестировать конечные точки HttpClient в модульных тестах.

NuGet: install-package WorldDomination.HttpClient.Helpers
Репо: https://github.com/PureKrome/HttpClient.Helpers

Основная идея заключается в том, что вы создаете фальшивый ответ и передаете FakeHttpMessageHandler экземпляр вашего кода, который включает в себя этот фальшивый ответ. Затем, когда ваш код пытается на самом деле попасть в конечную точку 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);
}

HttpClient используется внутри:

public class CustomAuthorizationAttribute : Attribute, IAuthorizationFilter
{
    private string Roles;
    private static readonly HttpClient _httpClient = new HttpClient();
Другие вопросы по тегам