Инъекция одного экземпляра HttpClient с определенным HttpMessageHandler

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

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

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

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

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

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

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

Ответ 1

Добавление к разговору из комментариев выглядит так, будто вам понадобится фабрика 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 от службы, чтобы клиент не стал детализацией реализации.

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

Ответ 2

Вы думаете, что сложно. Все, что вам нужно, это фабрика или метод доступа 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
...

Ответ 3

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

Предположим, что я делаю HTTP-запросы на example.com, у меня будет ExampleHttpClient который наследуется от HttpClient и имеет ту же самую подпись 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 как singleton в моей регистрации инъекций зависимостей и добавляю регистрацию для HttpMessageHandler как временную, поскольку она будет создана только один раз для каждого типа клиента. Используя этот шаблон, мне не нужно иметь несколько сложных регистраций для HttpClient или интеллектуальных фабрик для их создания на основе имени хоста назначения.

Все, что нужно поговорить с example.com, должно принимать зависимость конструктора от ExampleHttpClient а затем все они используют один и тот же экземпляр, и вы получаете пул соединений, как было разработано.

Этот способ также дает вам более удобное место для размещения таких вещей, как заголовки по умолчанию, типы контента, авторизация, базовый адрес и т.д. И помогает предотвратить настройку http для одной службы, протекающей в другой службе.

Ответ 4

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

NuGet: install-package WorldDomination.HttpClient.Helpers
Repo: 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);
}

Ответ 5

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

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