Use o IHttpClientFactory para implementar solicitações HTTP resilientes

Dica

Esse conteúdo é um trecho do eBook da Arquitetura de Microsserviços do .NET para os Aplicativos .NET em Contêineres, disponível no .NET Docs ou como um PDF para download gratuito que pode ser lido offline.

.NET Microservices Architecture for Containerized .NET Applications eBook cover thumbnail.

IHttpClientFactory é um contrato implementado por DefaultHttpClientFactory, um alocador "teimoso", disponível desde o .NET Core 2.1, para a criação de instâncias do HttpClient a serem usadas em seus aplicativos.

Problemas com a classe HttpClient original disponível no .NET

A classe HttpClient original e conhecida pode ser usada com facilidade, mas em alguns casos, muitos desenvolvedores não a usam corretamente.

Embora essa classe implemente IDisposable, declarar e instanciá-la dentro de uma instrução using não é preferível porque quando o objeto HttpClient é descartado, o soquete subjacente não é liberado imediatamente, o que pode levar a um problema de esgotamento do soquete. Para saber mais sobre esse problema, confira a postagem no blog You're using HttpClient wrong and it is destabilizing your software (Você está usando o HttpClient incorretamente e ele está desestabilizando o software).

Portanto, HttpClient deve ser instanciado uma única vez e reutilizado durante a vida útil de um aplicativo. A criação de uma instância de uma classe HttpClient para cada solicitação esgotará o número de soquetes disponíveis em condições de carga pesada. Esse problema resultará em erros de SocketException. Abordagens possíveis para resolver o problema baseiam-se na criação do objeto HttpClient como singleton ou estático, conforme é explicado neste artigo da Microsoft sobre o uso do HttpClient. Essa pode ser uma boa solução para aplicativos de console de curta duração ou semelhantes, que são executados algumas vezes por dia.

Outro problema que os desenvolvedores enfrentam é ao usar uma instância compartilhada de HttpClient em processos de longa execução. Em uma situação em que o HttpClient é instanciado como um singleton ou um objeto estático, ele não consegue lidar com as alterações de DNS, conforme descrito neste problema do repositório GitHub dotnet/runtime.

No entanto, o problema não está realmente no HttpClient em si, mas com o construtor padrão para HttpClient, pois ele cria uma nova instância concreta de HttpMessageHandler, que é aquela que enfrenta o esgotamento de soquetes e os problemas de alterações de DNS mencionados acima.

Para resolver os problemas mencionados acima e tornar as instâncias HttpClient gerenciáveis, o .NET Core 2.1 introduziu duas abordagens, sendo uma delas IHttpClientFactory. Trata-se de uma interface usada para configurar e criar instâncias HttpClient em um aplicativo por meio de DI (Injeção de Dependência). Ela também fornece extensões para o middleware baseado em Polly para aproveitar a delegação de manipuladores no HttpClient.

A alternativa é usar SocketsHttpHandler com PooledConnectionLifetime configurado. Essa abordagem é aplicada a instâncias de longa duração, static ou HttpClient singleton. Para saber mais sobre estratégias diferentes, confira Diretrizes de HttpClient para .NET.

Polly é uma biblioteca de manipulação de falhas transitórias que ajuda os desenvolvedores a adicionar resiliência aos seus aplicativos, usando algumas políticas predefinidas de maneira fluente e thread-safe.

Benefícios do uso de IHttpClientFactory

A implementação atual de IHttpClientFactory, que também implementa IHttpMessageHandlerFactory, oferece os seguintes benefícios:

  • Fornece um local central para nomear e configurar objetos lógicos de HttpClient. Por exemplo, você pode configurar um cliente (agente de serviço) pré-configurado para acessar um microsserviço específico.
  • Codifica o conceito de middleware de saída por meio da delegação de manipuladores no HttpClient e da implementação de middleware baseado em Polly para aproveitar as políticas da Polly e garantir a resiliência.
  • O HttpClient já tem o conceito de delegar manipuladores que podem ser vinculados uns aos outros para solicitações HTTP de saída. Você pode registrar clientes HTTP na fábrica e pode usar um manipulador Polly para usar políticas Polly para Repetição, CircuitBreakers etc.
  • Gerencie o tempo de vida de HttpMessageHandler para evitar os problemas mencionado que podem ocorrer ao gerenciar tempos de vida do HttpClient por conta própria.

Dica

As instâncias HttpClient injetadas por DI podem ser descartadas com segurança, pois o HttpMessageHandler associado é gerenciado pela fábrica. As instâncias HttpClient injetadas são Transitórias de uma perspectiva de DI, enquanto as instâncias HttpMessageHandler podem ser consideradas como Com escopo. As instâncias HttpMessageHandler têm os próprios escopos de DI, separados dos escopos do aplicativo (por exemplo, escopos de solicitação de entrada do ASP.NET). Para obter mais informações, consulte Como usar HttpClientFactory no .NET.

Observação

A implementação de IHttpClientFactory (DefaultHttpClientFactory) está fortemente vinculada à implementação de DI no pacote NuGet Microsoft.Extensions.DependencyInjection. Se você precisar usar HttpClient sem DI ou com outras implementações de DI, considere usar um static ou HttpClient singleton com PooledConnectionLifetime configurado. Para obter mais informações, confira Diretrizes de HttpClient para .NET.

Várias maneiras de usar IHttpClientFactory

Há várias maneiras de usar o IHttpClientFactory no aplicativo:

  • Uso básico
  • Usar clientes nomeados
  • Usar clientes tipados
  • Usar clientes gerados

Para fins de brevidade, essas diretrizes mostram a maneira mais estruturada de usar IHttpClientFactory, que é usar clientes tipados (padrão do agente de serviço). No entanto, todas as opções estão documentadas e listadas neste artigo que abrange o uso de IHttpClientFactory.

Observação

Se seu aplicativo exigir cookies, talvez seja melhor evitar o uso de IHttpClientFactory nele. Para obter maneiras alternativas de gerenciar clientes, consulte Diretrizes para usar clientes HTTP

Como usar clientes tipados com IHttpClientFactory

Portanto, o que é um "cliente tipado"? É apenas um HttpClient pré-configurado para algum uso específico. Essa configuração pode incluir valores específicos, como o servidor base, cabeçalhos HTTP ou tempos limite.

O diagrama a seguir mostra como os clientes tipados são usados com o IHttpClientFactory:

Diagram showing how typed clients are used with IHttpClientFactory.

Figura 8-4. Usando IHttpClientFactory com classes de cliente tipado.

Na imagem acima, um ClientService (usado por um controlador ou código do cliente) usa um HttpClient criado pelo IHttpClientFactory registrado. Esta fábrica atribui um HttpMessageHandler de um pool para o HttpClient. O HttpClient pode ser configurado com políticas da Polly ao registrar o IHttpClientFactory no contêiner de DI com o método de extensão AddHttpClient.

Para configurar a estrutura acima, adicione IHttpClientFactory em seu aplicativo instalando o pacote NuGet Microsoft.Extensions.Http que inclui o método de extensão AddHttpClient para IServiceCollection. Esse método de extensão registra a classe DefaultHttpClientFactory interna a ser usada como um singleton da interface IHttpClientFactory. Ele define uma configuração transitória para o HttpMessageHandlerBuilder. Esse manipulador de mensagens (objeto HttpMessageHandler), obtido de um pool, é usado pelo HttpClient retornado do alocador.

No próximo snippet, veja como AddHttpClient() pode ser usado para registrar clientes tipados (agentes de serviço) que precisam usar HttpClient.

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

O registro dos serviços do cliente, conforme mostrado no snippet anterior, faz com que o DefaultClientFactory crie um HttpClient padrão para cada serviço. O cliente tipado é registrado como transitório com o contêiner de DI. No código anterior, AddHttpClient() registra CatalogService, BasketService, OrderingService como serviços transitórios para que possam ser injetados e consumidos diretamente sem a necessidade de registros adicionais.

Você também pode adicionar uma configuração específica à instância no registro para, por exemplo, configurar o endereço base e adicionar algumas políticas de resiliência, conforme mostrado a seguir:

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

Neste próximo exemplo, você pode ver a configuração de uma das políticas acima:

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

Veja mais detalhes sobre como usar Polly no próximo artigo.

Tempos de vida de HttpClient

Sempre que você receber um objeto HttpClient do IHttpClientFactory, uma nova instância será retornada. Mas cada HttpClient usa um HttpMessageHandler que foi colocado em pool e reutilizado pelo IHttpClientFactory para reduzir o consumo de recursos, desde que o tempo de vida do HttpMessageHandler não tenha expirado.

O pooling de manipuladores é interessante porque cada manipulador normalmente gerencia suas próprias conexões de HTTP subjacentes. Criar mais manipuladores do que o necessário pode resultar em atrasos de conexão. Alguns manipuladores também mantêm as conexões abertas indefinidamente, o que pode impedir que o manipulador reaja a alterações de DNS.

Os objetos HttpMessageHandler no pool têm um tempo de vida que é o período de tempo em que uma instância HttpMessageHandler no pool pode ser reutilizada. O valor padrão é dois minutos, mas pode ser substituído por cliente tipado. Para substituí-lo, chame SetHandlerLifetime() no IHttpClientBuilder que é retornado ao criar o cliente, como mostra o código a seguir:

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

Cada cliente tipado pode ter seu próprio valor de tempo de vida do manipulador configurado. Defina o tempo de vida como InfiniteTimeSpan para desabilitar a expiração do manipulador.

Implementar suas classes de cliente tipado que usam o HttpClient injetado e configurado

Como uma etapa anterior, você precisa definir suas classes de Cliente Tipado, como as classes no código de exemplo: "BasketService", "CatalogService", "OrderingService" etc. Um cliente tipado é uma classe que aceita um objeto HttpClient (injetado por seu construtor) e o usa para chamar algum serviço HTTP remoto. Por exemplo:

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

O cliente tipado (CatalogService, no exemplo) é ativado pela DI (injeção de dependência), o que significa que ele pode aceitar qualquer serviço registrado em seu construtor, além do HttpClient.

Um cliente tipado é, efetivamente, um objeto transitório, ou seja, uma nova instância é criada sempre que há a necessidade. Ele recebe uma nova instância HttpClient sempre que é construída. No entanto, os objetos HttpMessageHandler no pool são os objetos que são reutilizados por várias instâncias HttpClient.

Usar suas classes de cliente tipado

Por fim, após a implementação de suas classes tipadas, você poderá registrá-las e configurá-las com AddHttpClient(). Depois disso, você poderá usá-los onde quer que os serviços sejam injetados por DI, como no código da página Razor ou em um controlador de aplicativo Web MVC, mostrado no código abaixo de 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
        }

        }
}

Até este ponto, o snippet de código acima mostra apenas o exemplo de execução de solicitações HTTP regulares. Mas a "mágica" acontece nas seções a seguir, em que mostra como todas as solicitações HTTP feitas por HttpClient, podem ter políticas resilientes, como repetições com retirada exponencial, disjuntores, recursos de segurança usando tokens de autenticação ou até mesmo qualquer outro recurso personalizado. E tudo isso pode ser feito apenas adicionando políticas e delegando manipuladores aos seus Clientes Tipados registrados.

Recursos adicionais