Использование IHttpClientFactory для реализации устойчивых HTTP-запросовUse IHttpClientFactory to implement resilient HTTP requests

IHttpClientFactory — это контракт, который реализуется DefaultHttpClientFactory и доступен, начиная с версии .NET Core 2.1. С его помощью можно создавать экземпляры HttpClient, которые используются в приложениях.IHttpClientFactory is a contract implemented by DefaultHttpClientFactory, an opinionated factory, available since .NET Core 2.1, for creating HttpClient instances to be used in your applications.

Проблемы с исходным классом HttpClient, доступным в .NET CoreIssues with the original HttpClient class available in .NET Core

Исходный и хорошо известный класс HttpClient очень просто использовать, но иногда разработчики применяют его неправильно.The original and well-known HttpClient class can be easily used, but in some cases, it isn't being properly used by many developers.

Этот класс реализует IDisposable, однако объявлять и создавать его экземпляры в инструкции using не рекомендуется, поскольку при удалении объекта HttpClient не происходит немедленное освобождение базового сокета, в результате чего со временем может возникнуть проблема нехватки сокетов.While this class implements IDisposable, declaring and instantiating it within a using statement is not preferred because when the HttpClient object gets disposed of, the underlying socket is not immediately released, which can lead to a socket exhaustion problem. Дополнительные сведения об этой проблеме см. в записи блога, посвященной неправильному использованию HttpClient и нарушению стабильной работы программного обеспечения.For more information about this issue, see the blog post You're using HttpClient wrong and it's destabilizing your software.

Таким образом, создается один экземпляр HttpClient, которые будет использоваться повторно на протяжении всего жизненного цикла приложения.Therefore, HttpClient is intended to be instantiated once and reused throughout the life of an application. Создание экземпляра класса HttpClient для каждого запроса будет сокращать количество доступных сокетов при больших нагрузках.Instantiating an HttpClient class for every request will exhaust the number of sockets available under heavy loads. В результате будут возникать ошибки SocketException.That issue will result in SocketException errors. Возможные способы решения этой проблемы основаны на создании объекта HttpClient в виде класса-одиночки или статического класса, как описано в этой статье Майкрософт об использовании HttpClient.Possible approaches to solve that problem are based on the creation of the HttpClient object as singleton or static, as explained in this Microsoft article on HttpClient usage. Это может быть хорошим решением для консольных приложений, которые выполняются непродолжительное время несколько раз в день, а также их аналогов.This can be a good solution for short-lived console apps or similar that are run a few times a day.

Кроме того, разработчики сталкиваются с проблемами при использовании общего экземпляра HttpClient в длительно выполняющихся процессах.Another issue that developers run into is when using a shared instance of HttpClient in long running processes. Если экземпляр HttpClient создается в единичном виде или как статический объект, он не может обрабатывать изменения DNS, как описывается в этой проблеме в репозитории GitHub dotnet/среды выполнения.In a situation where the HttpClient is instantiated as a singleton or a static object, it fails to handle the DNS changes as described in this issue of the dotnet/runtime GitHub repository.

Тем не менее эта проблема связана не с самим объектом HttpClient, а с конструктором по умолчанию для HttpClient, поскольку он создает новый конкретный экземпляр HttpMessageHandler, который является источником описываемых выше проблем, связанных с нехваткой сокетов и изменениями DNS.However, the issue isn't really with HttpClient per se, but with the default constructor for HttpClient, because it creates a new concrete instance of HttpMessageHandler, which is the one that has sockets exhaustion and DNS changes issues mentioned above.

Чтобы решить указанные выше проблемы и обеспечить возможность управления экземплярами HttpClient, в .NET Core 2.1 был представлен интерфейс IHttpClientFactory, который можно использовать для настройки и создания экземпляров HttpClient в приложении путем внедрения зависимостей.To address the issues mentioned above and to make HttpClient instances manageable, .NET Core 2.1 introduced the IHttpClientFactory interface which can be used to configure and create HttpClient instances in an app through Dependency Injection (DI). Также этот интерфейс предоставляет расширения для ПО промежуточного слоя на основе Polly, что позволяет использовать преимущества делегирования обработчиков в HttpClient.It also provides extensions for Polly-based middleware to take advantage of delegating handlers in HttpClient.

Polly — это библиотека для обеспечения обработки временных сбоев, которая позволяет разработчикам повысить устойчивость своих приложений, используя некоторые стандартные политики более эффективным и потокобезопасным способом.Polly is a transient-fault-handling library that helps developers add resiliency to their applications, by using some pre-defined policies in a fluent and thread-safe manner.

Преимущества использования IHttpClientFactoryBenefits of using IHttpClientFactory

В текущей реализации IHttpClientFactory также реализуется IHttpMessageHandlerFactory и предлагаются следующие преимущества.The current implementation of IHttpClientFactory, that also implements IHttpMessageHandlerFactory, offers the following benefits:

  • Центральное расположение для именования и настройки логических объектов HttpClient.Provides a central location for naming and configuring logical HttpClient objects. Например, вы можете настроить клиент (агент службы), который предварительно настроен для доступа к определенной микрослужбе.For example, you may configure a client (Service Agent) that's pre-configured to access a specific microservice.
  • Кодификация концепции исходящего ПО промежуточного слоя путем делегирования обработчиков в HttpClient и реализация ПО промежуточного слоя на основе Polly для использования политик устойчивости Polly.Codify the concept of outgoing middleware via delegating handlers in HttpClient and implementing Polly-based middleware to take advantage of Polly's policies for resiliency.
  • В HttpClient уже существует концепция делегирования обработчиков, которые можно связать друг с другом для исходящих HTTP-запросов.HttpClient already has the concept of delegating handlers that could be linked together for outgoing HTTP requests. Вы можете регистрировать клиенты HTTP в фабрике, а также использовать обработчик Polly, чтобы использовать политики Polly для повторных попыток, размыкателя цепи и т. д.You can register HTTP clients into the factory and you can use a Polly handler to use Polly policies for Retry, CircuitBreakers, and so on.
  • Управление временем существования HttpMessageHandler, чтобы избежать упомянутых проблем, которые могут возникнуть при управлении временем существования HttpClient самостоятельно.Manage the lifetime of HttpMessageHandler to avoid the mentioned problems/issues that can occur when managing HttpClient lifetimes yourself.

Совет

Экземпляры HttpClient, внедряемые в виде зависимостей, можно безопасно удалять, поскольку связанный с ними обработчик HttpMessageHandler управляется фабрикой.The HttpClient instances injected by DI, can be disposed of safely, because the associated HttpMessageHandler is managed by the factory. В сущности, внедренные экземпляры HttpClient с точки зрения внедрения зависимостей имеют ограниченную область действия.As a matter of fact, injected HttpClient instances are Scoped from a DI perspective.

Примечание

Реализация IHttpClientFactory (DefaultHttpClientFactory) тесно привязывается к реализации внедрения зависимостей в пакете NuGet Microsoft.Extensions.DependencyInjection.The implementation of IHttpClientFactory (DefaultHttpClientFactory) is tightly tied to the DI implementation in the Microsoft.Extensions.DependencyInjection NuGet package. См. сведения об использовании других контейнеров внедрения зависимостей в этом обсуждении GitHub.For more information about using other DI containers, see this GitHub discussion.

Способы применения IHttpClientFactoryMultiple ways to use IHttpClientFactory

Есть несколько способов использования IHttpClientFactory в вашем приложении:There are several ways that you can use IHttpClientFactory in your application:

  • Основное использованиеBasic usage
  • Использование именованных клиентовUse Named Clients
  • Использование типизированных клиентовUse Typed Clients
  • Использование созданных клиентовUse Generated Clients

Для краткости в этом руководстве показан наиболее структурированный способ использования IHttpClientFactory, а именно — с помощью типизированных клиентов (шаблон агента службы).For the sake of brevity, this guidance shows the most structured way to use IHttpClientFactory, which is to use Typed Clients (Service Agent pattern). Все параметры описаны и перечислены в этой статье, посвященной использованию IHttpClientFactory.However, all options are documented and are currently listed in this article covering the IHttpClientFactory usage.

Как использовать типизированные клиенты с IHttpClientFactoryHow to use Typed Clients with IHttpClientFactory

Так что же такое типизированный клиент?So, what's a "Typed Client"? Это просто объект HttpClient, предварительно настроенный для конкретной цели.It's just an HttpClient that's pre-configured for some specific use. Эта конфигурация может включать заданные значения, например базовый сервер, заголовки HTTP или величины времени ожидания.This configuration can include specific values such as the base server, HTTP headers or time outs.

На следующей схеме показано, как использовать типизированные клиенты с IHttpClientFactory.The following diagram shows how Typed Clients are used with IHttpClientFactory:

На схеме показано, как использовать типизированные клиенты с IHttpClientFactory.

Рис. 8-4.Figure 8-4. Использование IHttpClientFactory с классами типизированных клиентов.Using IHttpClientFactory with Typed Client classes.

На изображении выше ClientService (используется контроллером или в коде клиента) использует объект HttpClient, созданный зарегистрированной фабрикой IHttpClientFactory.In the above image, a ClientService (used by a controller or client code) uses an HttpClient created by the registered IHttpClientFactory. Эта фабрика назначает HttpMessageHandler из пула объекту HttpClient.This factory assigns an HttpMessageHandler from a pool to the HttpClient. HttpClient можно настроить с помощью политик Polly при регистрации фабрики IHttpClientFactory в контейнере внедрения зависимостей, используя метод расширения AddHttpClient.The HttpClient can be configured with Polly's policies when registering the IHttpClientFactory in the DI container with the extension method AddHttpClient.

Чтобы настроить такую структуру, добавьте IHttpClientFactory в приложение, установив пакет NuGet Microsoft.Extensions.Http, который содержит метод расширения AddHttpClient для IServiceCollection.To configure the above structure, add IHttpClientFactory in your application by installing the Microsoft.Extensions.Http NuGet package that includes the AddHttpClient extension method for IServiceCollection. Этот метод расширения регистрирует внутренний класс DefaultHttpClientFactory, который будет использоваться как класс-одиночка для интерфейса IHttpClientFactory.This extension method registers the internal DefaultHttpClientFactory class to be used as a singleton for the interface IHttpClientFactory. Он определяет временную конфигурацию для HttpMessageHandlerBuilder.It defines a transient configuration for the HttpMessageHandlerBuilder. Этот обработчик сообщений (объект HttpMessageHandler), взятый из пула, используется классом HttpClient, который возвращается фабрикой.This message handler (HttpMessageHandler object), taken from a pool, is used by the HttpClient returned from the factory.

В приведенном далее коде показано, как AddHttpClient() может использоваться для регистрации типизированных клиентов (агентов службы), которым нужно использовать HttpClient.In the next code, you can see how AddHttpClient() can be used to register Typed Clients (Service Agents) that need to use HttpClient.

// Startup.cs
//Add http client services at ConfigureServices(IServiceCollection services)
services.AddHttpClient<ICatalogService, CatalogService>();
services.AddHttpClient<IBasketService, BasketService>();
services.AddHttpClient<IOrderingService, OrderingService>();

Как показано в предыдущем примере кода, регистрация клиентских служб позволяет DefaultClientFactory создать объект HttpClient для каждой службы.Registering the client services as shown in the previous code, makes the DefaultClientFactory create a standard HttpClient for each service.

В регистрацию можно также добавить настройки конкретного экземпляра, например, настроить базовый адрес и добавить некоторые политики устойчивости, как показано в следующем коде:You could also add instance-specific configuration in the registration to, for example, configure the base address, and add some resiliency policies, as shown in the following code:

services.AddHttpClient<ICatalogService, CatalogService>(client =>
{
    client.BaseAddress = new Uri(Configuration["BaseUrl"]);
})
    .AddPolicyHandler(GetRetryPolicy())
    .AddPolicyHandler(GetCircuitBreakerPolicy());

В следующем коде для примера показана одна из приведенных выше политик.Just for the example sake, you can see one of the above policies in the next code:

static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
    return HttpPolicyExtensions
        .HandleTransientHttpError()
        .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)
        .WaitAndRetryAsync(6, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}

Дополнительные сведения об использовании Polly можно найти в следующей статье.You can find more details about using Polly in the Next article.

Время существования HttpClientHttpClient lifetimes

Каждый раз, когда вы получаете объект HttpClient из IHttpClientFactory, возвращается новый экземпляр.Each time you get an HttpClient object from the IHttpClientFactory, a new instance is returned. Но чтобы уменьшить потребление ресурсов, каждый объект HttpClient использует из пула экземпляр HttpMessageHandler, который также применяется фабрикой IHttpClientFactory, если время существования HttpMessageHandler еще не истекло.But each HttpClient uses an HttpMessageHandler that's pooled and reused by the IHttpClientFactory to reduce resource consumption, as long as the HttpMessageHandler's lifetime hasn't expired.

Создание пулов обработчиков желательно, поскольку каждый обработчик обычно управляет собственными базовыми HTTP-подключениями. Создание лишних обработчиков может привести к задержке подключения.Pooling of handlers is desirable as each handler typically manages its own underlying HTTP connections; creating more handlers than necessary can result in connection delays. Некоторые обработчики поддерживают подключения открытыми в течение неопределенного периода, что может помешать обработчику отреагировать на изменения DNS.Some handlers also keep connections open indefinitely, which can prevent the handler from reacting to DNS changes.

Время существования объектов HttpMessageHandlerэто период, в течение которого экземпляр HttpMessageHandler в пуле может использоваться повторно.The HttpMessageHandler objects in the pool have a lifetime that's the length of time that an HttpMessageHandler instance in the pool can be reused. Значение по умолчанию — две минуты, но его можно переопределить для отдельных типизированных клиентов.The default value is two minutes, but it can be overridden per Typed Client. Чтобы переопределить это значение, вызовите SetHandlerLifetime() в экземпляре IHttpClientBuilder, который возвращается при создании клиента, как показано в следующем примере кода:To override it, call SetHandlerLifetime() on the IHttpClientBuilder that's returned when creating the client, as shown in the following code:

//Set 5 min as the lifetime for the HttpMessageHandler objects in the pool used for the Catalog Typed Client
services.AddHttpClient<ICatalogService, CatalogService>()
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));

Для каждого типизированного клиента можно указать свое значение времени существования настроенного обработчика.Each Typed Client can have its own configured handler lifetime value. Установите значение InfiniteTimeSpan, чтобы отключить срок действия обработчика.Set the lifetime to InfiniteTimeSpan to disable handler expiry.

Реализация классов типизированных клиентов, использующих внедренный и настроенный HttpClientImplement your Typed Client classes that use the injected and configured HttpClient

Вы уже должны были определить классы типизированных клиентов, например классы в примере кода, такие как BasketService, CatalogService, OrderingService и т. д. Типизированный клиент — это класс, который принимает объект HttpClient (внедренный через конструктор) и использует его для вызова удаленной службы HTTP.As a previous step, you need to have your Typed Client classes defined, such as the classes in the sample code, like 'BasketService', 'CatalogService', 'OrderingService', etc. – A Typed Client is a class that accepts an HttpClient object (injected through its constructor) and uses it to call some remote HTTP service. Пример:For example:

public class CatalogService : ICatalogService
{
    private readonly HttpClient _httpClient;
    private readonly string _remoteServiceBaseUrl;

    public CatalogService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<Catalog> GetCatalogItems(int page, int take,
                                               int? brand, int? type)
    {
        var uri = API.Catalog.GetAllCatalogItems(_remoteServiceBaseUrl,
                                                 page, take, brand, type);

        var responseString = await _httpClient.GetStringAsync(uri);

        var catalog = JsonConvert.DeserializeObject<Catalog>(responseString);
        return catalog;
    }
}

Типизированный клиент (в нашем примере это CatalogService) активируется путем внедрения зависимостей, то есть он может принять любую зарегистрированную службу в свой конструктор в дополнение к HttpClient.The Typed Client (CatalogService in the example) is activated by DI (Dependency Injection), meaning that it can accept any registered service in its constructor, in addition to HttpClient.

По сути, типизированный клиент — это временный объект, то есть экземпляр создается каждый раз по необходимости и при этом он будет получать новый экземпляр HttpClient.A Typed Client is, effectively, a transient object, meaning that a new instance is created each time one is needed and it will receive a new HttpClient instance each time it's constructed. Тем не менее объекты HttpMessageHandler в пуле используются повторно множеством экземпляров HttpClient.However, the HttpMessageHandler objects in the pool are the objects that are reused by multiple HttpClient instances.

Использование классов типизированных клиентовUse your Typed Client classes

Наконец, когда вы реализуете классы типов, а также зарегистрируете и настроите их в AddHttpClient(), их можно будет использовать везде, где можно внедрить службы с помощью внедрения зависимостей.Finally, once you have your typed classes implemented and have them registered and configured with AddHttpClient(), you can use them wherever you can have services injected by DI. Например, в любом коде страницы Razor или любом контроллере веб-приложения MVC, как в следующем коде из eShopOnContainers.For example, in a Razor page code or controller of an MVC web app, like in the following code from eShopOnContainers:

namespace Microsoft.eShopOnContainers.WebMVC.Controllers
{
    public class CatalogController : Controller
    {
        private ICatalogService _catalogSvc;

        public CatalogController(ICatalogService catalogSvc) =>
                                                           _catalogSvc = catalogSvc;

        public async Task<IActionResult> Index(int? BrandFilterApplied,
                                               int? TypesFilterApplied,
                                               int? page,
                                               [FromQuery]string errorMsg)
        {
            var itemsPage = 10;
            var catalog = await _catalogSvc.GetCatalogItems(page ?? 0,
                                                            itemsPage,
                                                            BrandFilterApplied,
                                                            TypesFilterApplied);
            //… Additional code
        }

        }
}

Пока пример кода просто выполняет обычные HTTP-запросы. Самое интересное начнется в следующих разделах, где вы будете просто добавлять политики и делегирующие обработчики в зарегистрированные типизированные клиенты. Все HTTP-запросы, которые должны выполняться объектом HttpClient, будут учитывать политики отказоустойчивости, например политики повтора c экспоненциальной задержкой, размыкатели цепи или другие настраиваемые делегирующие обработчики, чтобы реализовать дополнительные функции безопасности, такие как маркеры проверки подлинности и другие настраиваемые возможности.Up to this point, the code shown is just performing regular Http requests, but the 'magic' comes in the following sections where, just by adding policies and delegating handlers to your registered Typed Clients, all the HTTP requests to be done by HttpClient will behave taking into account resilient policies such as retries with exponential backoff, circuit breakers, or any other custom delegating handler to implement additional security features, like using auth tokens, or any other custom feature.

Дополнительные ресурсыAdditional resources