IHttpClientFactory com .NET

Neste artigo, você aprenderá a usar o IHttpClientFactory para criar tipos HttpClient com vários conceitos básicos do .NET, como DI (injeção de dependência), registro em log e configuração. O tipo HttpClient foi introduzido no .NET Framework 4.5, que foi lançado em 2012. Em outras palavras, já existe há algum tempo. HttpClient é usado para fazer solicitações HTTP e lidar com respostas HTTP de recursos da Web identificados por um Uri. O protocolo HTTP compõe a grande maioria de todo o tráfego da Internet.

Com os princípios de desenvolvimento de aplicativos modernos que conduzem as práticas recomendadas, o IHttpClientFactory serve como uma abstração de fábrica que pode criar instâncias HttpClient com configurações personalizadas. IHttpClientFactory foi introduzido no .NET Core 2.1. Cargas de trabalho comuns do .NET baseadas em HTTP podem aproveitar o middleware de terceiros resiliente e transitório de manipulação de falhas com facilidade.

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.

Importante

O gerenciamento de tempo de vida das instâncias HttpClient criadas pelo IHttpClientFactory é completamente diferente das instâncias criadas manualmente. As estratégias são usar clientes de curta duração criados por IHttpClientFactory ou clientes de longa duração com PooledConnectionLifetime configurado. Para obter mais informações, consulte a seção Gerenciamento de tempo de vida do HttpClient e Diretrizes para usar clientes HTTP.

O tipo IHttpClientFactory.

Todo o código-fonte de exemplo neste artigo depende dos pacotes do NuGet Microsoft.Extensions.Http e . Além disso, solicitações GET HTTP são feitas para a API de Espaço Reservado {JSON} gratuita para obter objetos Todo de usuário.

Quando você chama qualquer um dos métodos de extensão AddHttpClient, você está adicionando IHttpClientFactory e serviços e relacionados ao IServiceCollection. O tipo IHttpClientFactory oferece os seguintes benefícios:

  • Expõe a classe HttpClient como um tipo pronto para DI.
  • Fornece um local central para nomear e configurar instâncias lógicas de HttpClient.
  • Codifica o conceito de middleware de saída por meio da delegação de manipuladores em HttpClient.
  • Fornece métodos de extensão para o middleware baseado em Polly para aproveitar a delegação de manipuladores em HttpClient.
  • Gerencia o cache e o tempo de vida das instâncias HttpClientHandler subjacentes. O gerenciamento automático evita problemas comuns do DNS (Sistema de Nomes de Domínio) que ocorrem ao gerenciar manualmente o tempos de vida HttpClient.
  • Adiciona uma experiência de registro em log configurável (via ILogger) para todas as solicitações enviadas por meio de clientes criados pelo alocador.

Padrões de consumo

Há várias maneiras de usar o IHttpClientFactory em um aplicativo:

A melhor abordagem depende dos requisitos do aplicativo.

Uso básico

Para registrar IHttpClientFactory, chame AddHttpClient:

using Shared;
using BasicHttp.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddHttpClient();
builder.Services.AddTransient<TodoService>();

using IHost host = builder.Build();

O consumo de serviços pode exigir o IHttpClientFactory como parâmetro construtor com DI. O código a seguir usa IHttpClientFactory para criar uma instância HttpClient.

using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Shared;

namespace BasicHttp.Example;

public sealed class TodoService(
    IHttpClientFactory httpClientFactory,
    ILogger<TodoService> logger)
{
    public async Task<Todo[]> GetUserTodosAsync(int userId)
    {
        // Create the client
        using HttpClient client = httpClientFactory.CreateClient();
        
        try
        {
            // Make HTTP GET request
            // Parse JSON response deserialize into Todo types
            Todo[]? todos = await client.GetFromJsonAsync<Todo[]>(
                $"https://jsonplaceholder.typicode.com/todos?userId={userId}",
                new JsonSerializerOptions(JsonSerializerDefaults.Web));

            return todos ?? [];
        }
        catch (Exception ex)
        {
            logger.LogError("Error getting something fun to say: {Error}", ex);
        }

        return [];
    }
}

Usar IHttpClientFactory como no exemplo anterior é uma boa maneira de refatorar um aplicativo existente. Não há nenhum impacto na maneira em que HttpClient é usado. Em locais em que as instâncias HttpClient são criadas em um aplicativo existente, substitua essas ocorrências por chamadas para CreateClient.

Clientes nomeados

Clientes nomeados são uma boa opção quando:

  • O aplicativo requer muitos usos distintos de HttpClient.
  • Muitas instâncias HttpClient têm configurações diferentes.

A configuração de um HttpClient nomeado pode ser especificada durante o registro no IServiceCollection:

using Shared;
using NamedHttp.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

string? httpClientName = builder.Configuration["TodoHttpClientName"];
ArgumentException.ThrowIfNullOrEmpty(httpClientName);

builder.Services.AddHttpClient(
    httpClientName,
    client =>
    {
        // Set the base address of the named client.
        client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");

        // Add a user-agent default request header.
        client.DefaultRequestHeaders.UserAgent.ParseAdd("dotnet-docs");
    });

No código anterior, o cliente é configurado com:

  • Um nome que é extraído da configuração sob o "TodoHttpClientName".
  • O endereço básico https://jsonplaceholder.typicode.com/.
  • Um cabeçalho "User-Agent".

Você pode usar a configuração para especificar nomes de cliente HTTP, o que é útil para evitar nomes errados de clientes ao adicionar e criar. Neste exemplo, o arquivo appsettings.json é usado para configurar o nome do cliente HTTP:

{
    "TodoHttpClientName": "JsonPlaceholderApi"
}

É fácil estender essa configuração e armazenar mais detalhes sobre como você gostaria que seu cliente HTTP funcionasse. Para obter mais informações, confira Configuração no .NET.

Criar cliente

Cada vez que CreateClient é chamado:

  • Uma nova instância de HttpClient é criada.
  • A ação de configuração é chamada.

Para criar um cliente nomeado, passe o nome dele para CreateClient:

using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Shared;

namespace NamedHttp.Example;

public sealed class TodoService
{
    private readonly IHttpClientFactory _httpClientFactory = null!;
    private readonly IConfiguration _configuration = null!;
    private readonly ILogger<TodoService> _logger = null!;

    public TodoService(
        IHttpClientFactory httpClientFactory,
        IConfiguration configuration,
        ILogger<TodoService> logger) =>
        (_httpClientFactory, _configuration, _logger) =
            (httpClientFactory, configuration, logger);

    public async Task<Todo[]> GetUserTodosAsync(int userId)
    {
        // Create the client
        string? httpClientName = _configuration["TodoHttpClientName"];
        using HttpClient client = _httpClientFactory.CreateClient(httpClientName ?? "");

        try
        {
            // Make HTTP GET request
            // Parse JSON response deserialize into Todo type
            Todo[]? todos = await client.GetFromJsonAsync<Todo[]>(
                $"todos?userId={userId}",
                new JsonSerializerOptions(JsonSerializerDefaults.Web));

            return todos ?? [];
        }
        catch (Exception ex)
        {
            _logger.LogError("Error getting something fun to say: {Error}", ex);
        }

        return [];
    }
}

No código anterior, a solicitação HTTP não precisa especificar um nome do host. O código pode passar apenas o caminho, pois o endereço básico configurado para o cliente é usado.

Clientes com tipo

Clientes com tipo:

  • Fornecem as mesmas funcionalidade que os clientes nomeados sem a necessidade de usar cadeias de caracteres como chaves.
  • Fornecem a ajuda do IntelliSense e do compilador durante o consumo de clientes.
  • Fornecem um único local para configurar e interagir com um determinado HttpClient. Por exemplo, um único cliente com tipo pode ser usado:
    • Para um único ponto de extremidade de back-end.
    • Para encapsular toda a lógica que lida com o ponto de extremidade.
  • Funcionam com a DI e podem ser injetados no local necessário no aplicativo.

Um cliente com tipo aceita um parâmetro HttpClient em seu construtor:

using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Shared;

namespace TypedHttp.Example;

public sealed class TodoService(
    HttpClient httpClient,
    ILogger<TodoService> logger) : IDisposable
{
    public async Task<Todo[]> GetUserTodosAsync(int userId)
    {
        try
        {
            // Make HTTP GET request
            // Parse JSON response deserialize into Todo type
            Todo[]? todos = await httpClient.GetFromJsonAsync<Todo[]>(
                $"todos?userId={userId}",
                new JsonSerializerOptions(JsonSerializerDefaults.Web));

            return todos ?? [];
        }
        catch (Exception ex)
        {
            logger.LogError("Error getting something fun to say: {Error}", ex);
        }

        return [];
    }

    public void Dispose() => httpClient?.Dispose();
}

No código anterior:

  • A configuração é definida quando o cliente com tipo é adicionado à coleção de serviços.
  • A opção HttpClient é atribuída como uma variável de escopo de classe (campo) e usada com APIs expostas.

Métodos específicos da API podem ser criados que expõem a funcionalidade HttpClient. Por exemplo, o método GetUserTodosAsync encapsula o código para recuperar objetos Todo específicos do usuário.

O código a seguir chamaAddHttpClient para registrar uma classe de cliente com tipo:

using Shared;
using TypedHttp.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddHttpClient<TodoService>(
    client =>
    {
        // Set the base address of the typed client.
        client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");

        // Add a user-agent default request header.
        client.DefaultRequestHeaders.UserAgent.ParseAdd("dotnet-docs");
    });

O cliente com tipo é registrado como transitório com a DI. No código anterior, AddHttpClient registra TodoService como um serviço transitório. Esse registro usa um método de fábrica para:

  1. Crie uma instância de HttpClient.
  2. Crie uma instância de TodoService, passando a instância de HttpClient para seu construtor.

Importante

O uso de clientes tipados em serviços singleton pode ser perigoso. Para obter mais informações, consulte a seção Evitar clientes tipados em serviços singleton.

Observação

Ao registrar um cliente tipado com o método AddHttpClient<TClient>, o tipo TClient deve ter um construtor que aceite um parâmetro HttpClient. Além disso, o tipo TClient não deve ser registrado com o contêiner de DI separadamente.

Clientes nomeados e tipados

Clientes nomeados e clientes tipados têm seus pontos fortes e fracos. Há uma maneira de combinar esses dois tipos de cliente para obter o melhor dos dois mundos.

O caso de uso primário é o seguinte: use o mesmo cliente tipado, mas em domínios diferentes. Por exemplo, você tem um serviço primário e secundário e eles expõem exatamente a mesma funcionalidade. Isso significa que você pode usar o mesmo cliente tipado para encapsular o uso de HttpClient para emitir solicitações, processar respostas e lidar com erros. O mesmo código será usado, mas com configurações diferentes (endereço base, tempo limite e credenciais diferentes, por exemplo).

O exemplo a seguir usa o mesmo cliente tipado TodoService, que foi mostrado na seção clientes tipados.

Primeiro, registre os clientes nomeados e tipados.

using Shared;
using TypedHttp.Example;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddHttpClient<TodoService>("primary"
    client =>
    {
        // Configure the primary typed client
        client.BaseAddress = new Uri("https://primary-host-address.com/");
        client.Timeout = TimeSpan.FromSeconds(3);
    });

// Register the same typed client but with different settings
builder.Services.AddHttpClient<TodoService>("secondary"
    client =>
    {
        // Configure the secondary typed client
        client.BaseAddress = new Uri("https://secondary-host-address.com/");
        client.Timeout = TimeSpan.FromSeconds(10);
    });

No código anterior:

  • A primeira chamada AddHttpClient registra um cliente TodoService digitado com o nome primary. O HttpClient subjacente aponta para o serviço primário e tem um tempo limite curto.
  • A segunda chamada AddHttpClient registra um cliente TodoService digitado com o nome secondary. O HttpClient subjacente aponta para o serviço secundário e tem um tempo limite maior.
using IHost host = builder.Build();

// Fetch an IHttpClientFactory instance to create a named client
IHttpClientFactory namedClientFactory =
    host.Services.GetRequiredService<IHttpClientFactory>();

// Fetch an ITypedHttpClientFactory<TodoService> instance to create a named and typed client
ITypedHttpClientFactory<TodoService> typedClientFactory  =
    host.Services.GetRequiredService<ITypedHttpClientFactory<TodoService>>();

// Create a TodoService instance against the primary host
var primaryClient = namedClientFactory.CreateClient("primary");
var todoService = typedClientFactory.CreateClient(primaryClient);

No código anterior:

  • Uma instância IHttpClientFactory é recuperada do contêiner de DI para poder criar clientes nomeados por meio do método CreateClient dela.
  • Uma instância ITypedHttpClientFactory<TodoService> é recuperada do contêiner de DI para poder criar clientes tipados por meio do método CreateClient dela.
    • Essa sobrecarga de CreateClient recebeu um HttpClient nomeado (com a configuração adequada) como parâmetro.
    • O todoService criado é configurado para usar o serviço primário.

Observação

O tipo IHttpClientFactory reside dentro dos namespaces System.Net.Http, enquanto o tipo ITypedHttpClientFactory reside dentro do Microsoft.Extensions.Http.

Importante

Use a classe de implementação (no exemplo anterior, o TodoService) como o parâmetro de tipo para o ITypedHttpClientFactory. Mesmo que você também tenha uma abstração (como a interface ITodoService), você ainda precisará usar a implementação. Se você usar acidentalmente a abstração (ITodoService) quando chamar o CreateClient dela, ela gerará uma InvalidOperationException.

try
{
    Todo[] todos = await todoService.GetUserTodosAsync(4);
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
{
    // The request timed out against the primary host

    // Create a TodoService instance against the secondary host
    var fallbackClient = namedClientFactory.CreateClient("secondary");
    var todoFallbackService = typedClientFactory.CreateClient(fallbackClient);

    // Issue request against the secondary host
    Todo[] todos = await todoFallbackService.GetUserTodosAsync(4);
}

No código anterior:

  • Ela tenta emitir uma solicitação para o serviço primário.
  • Se a solicitação atingir o tempo limite (levar mais de três segundos), ela gerará uma TaskCanceledException com uma TimeoutException interna.
  • No caso de atingir um tempo limite, um novo cliente é criado e usado, que agora está direcionado ao serviço secundário.

Clientes gerados

IHttpClientFactory pode ser usado em combinação com bibliotecas de terceiros, como a Refit. Refit é uma biblioteca REST para .NET. Ele permite definições declarativas de API REST, mapeando métodos de interface para pontos de extremidade. Uma implementação da interface é gerada dinamicamente pelo RestService usando HttpClient para fazer as chamadas de HTTP externas.

Considere o seguinte tipo record:

namespace Shared;

public record class Todo(
    int UserId,
    int Id,
    string Title,
    bool Completed);

O exemplo a seguir depende do pacote NuGet Refit.HttpClientFactory e é uma interface simples:

using Refit;
using Shared;

namespace GeneratedHttp.Example;

public interface ITodoService
{
    [Get("/todos?userId={userId}")]
    Task<Todo[]> GetUserTodosAsync(int userId);
}

A interface C# anterior:

  • Define um método chamado GetUserTodosAsync que retorna uma instância Task<Todo[]>.
  • Declara um atributo Refit.GetAttribute com o caminho e a cadeia de caracteres de consulta para a API externa.

Um cliente com tipo pode ser adicionado usando a Refit para gerar a implementação:

using GeneratedHttp.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Refit;
using Shared;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddRefitClient<ITodoService>()
    .ConfigureHttpClient(client =>
    {
        // Set the base address of the named client.
        client.BaseAddress = new Uri("https://jsonplaceholder.typicode.com/");

        // Add a user-agent default request header.
        client.DefaultRequestHeaders.UserAgent.ParseAdd("dotnet-docs");
    });

A interface definida pode ser consumida, quando necessário, com a implementação fornecida pela DI e pela Refit.

Fazer solicitações POST, PUT e DELETE

Nos exemplos anteriores, todas as solicitações HTTP usam o verbo HTTP GET. HttpClient também dá suporte a outros verbos HTTP, incluindo:

  • POST
  • PUT
  • DELETE
  • PATCH

Para obter uma lista completa de verbos HTTP compatíveis, consulte HttpMethod. Para obter mais informações sobre como fazer solicitações HTTP, confira Enviar uma solicitação usando HttpClient.

O exemplo a seguir mostra como fazer uma solicitação POST HTTP:

public async Task CreateItemAsync(Item item)
{
    using StringContent json = new(
        JsonSerializer.Serialize(item, new JsonSerializerOptions(JsonSerializerDefaults.Web)),
        Encoding.UTF8,
        MediaTypeNames.Application.Json);

    using HttpResponseMessage httpResponse =
        await httpClient.PostAsync("/api/items", json);

    httpResponse.EnsureSuccessStatusCode();
}

No código anterior, o método CreateItemAsync:

  • Serializa o parâmetro Item para JSON usando System.Text.Json. Ele usa uma instância de JsonSerializerOptions para configurar o processo de serialização.
  • Cria uma instância de StringContent para empacotar o JSON serializado para enviar o corpo da solicitação HTTP.
  • Chama PostAsync para enviar o conteúdo JSON para a URL especificada. Essa é uma URL relativa que é adicionada ao HttpClient.BaseAddress.
  • Chama EnsureSuccessStatusCode para gerar uma exceção se o código de status de resposta não indicar êxito.

HttpClient também dá suporte a outros tipos de conteúdo. Por exemplo, MultipartContent e StreamContent. Para obter uma lista completa dos dispositivos com suporte, consulte HttpContent.

O exemplo a seguir mostra uma solicitação PUT HTTP:

public async Task UpdateItemAsync(Item item)
{
    using StringContent json = new(
        JsonSerializer.Serialize(item, new JsonSerializerOptions(JsonSerializerDefaults.Web)),
        Encoding.UTF8,
        MediaTypeNames.Application.Json);

    using HttpResponseMessage httpResponse =
        await httpClient.PutAsync($"/api/items/{item.Id}", json);

    httpResponse.EnsureSuccessStatusCode();
}

O código anterior é muito semelhante ao exemplo POST. O método UpdateItemAsync chama PutAsync em vez de PostAsync.

O exemplo a seguir mostra uma solicitação DELETE HTTP:

public async Task DeleteItemAsync(Guid id)
{
    using HttpResponseMessage httpResponse =
        await httpClient.DeleteAsync($"/api/items/{id}");

    httpResponse.EnsureSuccessStatusCode();
}

No código anterior, o método DeleteItemAsync chama DeleteAsync. Como as solicitações HTTP DELETE normalmente não contêm corpo, o método DeleteAsync não fornece uma sobrecarga que aceita uma instância de HttpContent.

Para saber mais sobre como usar verbos HTTP diferentes com HttpClient, consulte HttpClient.

HttpClient gerenciamento do tempo de vida

Uma nova instância de HttpClient é chamada, sempre que CreateClient é chamado na IHttpClientFactory. Uma instância HttpClientHandler é criada por nome do cliente. O alocador gerencia a vida útil das instâncias do HttpClientHandler.

IHttpClientFactory armazena em cache as instâncias de HttpClientHandler criadas pelo alocador para reduzir o consumo de recursos. Uma instância de HttpClientHandler poderá ser reutilizada do cache, ao criar uma nova instância de HttpClient, se o respectivo tempo de vida não tiver expirado.

O cache de manipuladores é preferível porque normalmente cada manipulador gerencia o próprio pool de conexão HTTP subjacente. Criar mais manipuladores do que o necessário pode resultar no esgotamento do soquete e 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.

O tempo de vida padrão do manipulador é de 2 minutos. Para substituir o valor padrão, chame SetHandlerLifetime para cada cliente, no IServiceCollection:

services.AddHttpClient("Named.Client")
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));

Importante

As instâncias HttpClient criadas por IHttpClientFactory devem ser de curta duração.

  • Reciclar e recriar HttpMessageHandlers quando o tempo de vida deles expira é essencial para IHttpClientFactory garantir que os manipuladores reajam às alterações de DNS. HttpClient é vinculado a uma instância de manipulador específica no momento de sua criação, portanto, novas instâncias HttpClient devem ser solicitadas em tempo hábil para garantir que o cliente receberá o manipulador atualizado.

  • O descarte dessas instâncias HttpClientcriadas pelo alocador não leva ao esgotamento do soquete, pois seu descarte não dispara o descarte do HttpMessageHandler. IHttpClientFactory rastreia e descarta os recursos usados para criar instâncias HttpClient, especificamente as instâncias HttpMessageHandler, assim que o tempo de vida delas expira e HttpClient não as utiliza mais.

Manter uma só instância HttpClient ativa por uma longa duração é um padrão comum que pode ser usado como uma alternativa a IHttpClientFactory, no entanto, esse padrão requer configuração adicional, como PooledConnectionLifetime. Você pode usar clientes de longa duração com PooledConnectionLifetime ou clientes de curta duração criados pelo IHttpClientFactory. Para obter informações sobre qual estratégia usar em seu aplicativo, consulte Diretrizes para usar clientes HTTP.

Configurar o HttpMessageHandler

Talvez seja necessário controlar a configuração do HttpMessageHandler interno usado por um cliente.

Um IHttpClientBuilder é retornado ao adicionar clientes nomeados ou com tipo. O método de extensão ConfigurePrimaryHttpMessageHandler pode ser usado para definir um representante no IServiceCollection. O representante que é usado para criar e configurar o HttpMessageHandler primário usado pelo cliente:

.ConfigurePrimaryHttpMessageHandler(() =>
{
    return new HttpClientHandler
    {
        AllowAutoRedirect = false,
        UseDefaultCredentials = true
    };
});

Configurar o HttClientHandler permite que você especifique um proxy para a instância de HttpClient entre várias outras propriedades do manipulador. Para obter mais informações, consulte Proxy por cliente.

Configuração adicional

Há várias opções de configuração adicionais para controlar o IHttpClientHandler:

Método Descrição
AddHttpMessageHandler Adiciona um manipulador de mensagens adicional para um chamado HttpClient.
AddTypedClient Configura a associação entre o TClient e o HttpClient nomeado associado a IHttpClientBuilder.
ConfigureHttpClient Adiciona um delegado que será usado para configurar um HttpClient nomeado.
ConfigureHttpMessageHandlerBuilder Adiciona um delegado que será usado para configurar manipuladores de mensagens usando HttpMessageHandlerBuilder para um HttpClient nomeado.
ConfigurePrimaryHttpMessageHandler Configura o HttpMessageHandler primário do contêiner de injeção de dependência para um HttpClient nomeado.
RedactLoggedHeaders Define a coleção de nomes de cabeçalho HTTP para os quais os valores devem ser reeditados antes do registro em log.
SetHandlerLifetime Define o período em que uma instância de HttpMessageHandler pode ser reutilizada. Cada cliente nomeado pode ter o próprio valor de tempo de vida do manipulador configurado.

Usando IHttpClientFactory com SocketsHttpHandler

A implementação SocketsHttpHandler de HttpMessageHandler foi adicionada ao .NET Core 2.1, que permite a configuração de PooledConnectionLifetime. Essa configuração é usada para garantir que o manipulador reaja às alterações de DNS, portanto, usar SocketsHttpHandler é considerado uma alternativa ao uso de IHttpClientFactory. Para obter mais informações, confira Diretrizes para usar clientes HTTP.

No entanto, SocketsHttpHandler e IHttpClientFactory podem ser usados juntos para melhorar a configurabilidade. Usando essas duas APIs, você se beneficia da configurabilidade em nível baixo (por exemplo, usar LocalCertificateSelectionCallback para seleção de certificado dinâmico) e em nível alto (por exemplo, aproveitar a integração de DI e várias configurações de cliente).

Para usar as duas APIs:

  1. Especifique SocketsHttpHandler como PrimaryHandler e configure seu PooledConnectionLifetime (por exemplo, para um valor que estava em HandlerLifetime).
  2. Assim como SocketsHttpHandler lidará com o pool de conexões e com a reciclagem, a reciclagem do manipulador no nível IHttpClientFactory não é mais necessária. Você pode desabilitá-lo definindo HandlerLifetime como Timeout.InfiniteTimeSpan.
services.AddHttpClient(name)
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new SocketsHttpHandler()
        {
            PooledConnectionLifetime = TimeSpan.FromMinutes(2)
        };
    })
    .SetHandlerLifetime(Timeout.InfiniteTimeSpan); // Disable rotation, as it is handled by PooledConnectionLifetime

Evitar clientes tipados em serviços singleton

Ao usar a abordagem de cliente nomeado, IHttpClientFactory é injetado em serviços e instâncias HttpClient são criadas chamando CreateClient sempre que um HttpClient é necessário.

No entanto, com a abordagem de cliente tipado, os clientes tipados são objetos transitórios geralmente injetados em serviços. Isso pode causar um problema porque um cliente tipado pode ser injetado em um serviço singleton.

Importante

Espera-se que os clientes tipados sejam de curta duração no mesmo sentido das instâncias HttpClient criadas por IHttpClientFactory (para obter mais informações, consulte gerenciamento de tempo de vida de HttpClient). Assim que uma instância de cliente tipado é criada, IHttpClientFactory não tem controle sobre ela. Se uma instância de cliente tipado for capturada em um singleton, ela poderá impedir que ele reaja às alterações de DNS, eliminando uma das finalidades de IHttpClientFactory.

Se você precisar usar instâncias HttpClient em um serviço singleton, considere as seguintes opções:

  • Use a abordagem de cliente nomeado, injetando IHttpClientFactory no serviço singleton e recriando instâncias HttpClient quando necessário.
  • Se você precisar da abordagem de cliente tipado, use SocketsHttpHandler com PooledConnectionLifetime configurado como um manipulador primário. Para obter mais informações sobre como usar SocketsHttpHandler com IHttpClientFactory, consulte a seção Usando IHttpClientFactory com SocketsHttpHandler.

Escopos do manipulador de mensagens em IHttpClientFactory

IHttpClientFactory cria um escopo de DI separado para cada instância HttpMessageHandler. Esses escopos de DI são separados dos escopos de DI do aplicativo (por exemplo, escopo de solicitação de entrada de ASP.NET ou escopo de DI manual criado pelo usuário), portanto, eles não compartilham instâncias de serviço com escopo. Os escopos do Manipulador de Mensagens estão vinculados ao tempo de vida do manipulador e podem durar mais que os escopos do aplicativo, o que pode levar, por exemplo, a reutilização da mesma instância HttpMessageHandler com as mesmas dependências com escopo injetadas entre várias solicitações de entrada.

Diagrama mostrando dois escopos de DI do aplicativo e um escopo de manipulador de mensagens separado

Os usuários são altamente aconselhados a não armazenar em cache informações relacionadas ao escopo (como dados de HttpContext) dentro de instâncias HttpMessageHandler e usar dependências com escopo com cuidado para evitar o vazamento de informações confidenciais.

Se precisasse de acesso a um escopo de DI de aplicativo do manipulador de mensagens, para autenticação por exemplo, você encapsularia a lógica com reconhecimento de escopo em um DelegatingHandler transitório separado, e a encapsularia em torno de uma instância de HttpMessageHandler do cache IHttpClientFactory. Para acessar o manipulador, chame IHttpMessageHandlerFactory.CreateHandler para qualquer cliente nomeado registrado. Nesse caso, você mesmo criaria uma instância de HttpClient usando o manipulador construído.

Diagrama mostrando a obtenção de acesso aos escopos de DI do aplicativo por meio de um manipulador de mensagens transitório separado e IHttpMessageHandlerFactory

O exemplo a seguir mostra a criação de um HttpClient com um DelegatingHandler com reconhecimento de escopo:

if (scopeAwareHandlerType != null)
{
    if (!typeof(DelegatingHandler).IsAssignableFrom(scopeAwareHandlerType))
    {
        throw new ArgumentException($"""
            Scope aware HttpHandler {scopeAwareHandlerType.Name} should
            be assignable to DelegatingHandler
            """);
    }

    // Create top-most delegating handler with scoped dependencies
    scopeAwareHandler = (DelegatingHandler)_scopeServiceProvider.GetRequiredService(scopeAwareHandlerType); // should be transient
    if (scopeAwareHandler.InnerHandler != null)
    {
        throw new ArgumentException($"""
            Inner handler of a delegating handler {scopeAwareHandlerType.Name} should be null.
            Scope aware HttpHandler should be registered as Transient.
            """);
    }
}

// Get or create HttpMessageHandler from HttpClientFactory
HttpMessageHandler handler = _httpMessageHandlerFactory.CreateHandler(name);

if (scopeAwareHandler != null)
{
    scopeAwareHandler.InnerHandler = handler;
    handler = scopeAwareHandler;
}

HttpClient client = new(handler);

Outra solução alternativa pode seguir com um método de extensão para registrar um DelegatingHandler com reconhecimento de escopo e substituir o registro IHttpClientFactory padrão por um serviço transitório com acesso ao escopo atual do aplicativo:

public static IHttpClientBuilder AddScopeAwareHttpHandler<THandler>(
    this IHttpClientBuilder builder) where THandler : DelegatingHandler
{
    builder.Services.TryAddTransient<THandler>();
    if (!builder.Services.Any(sd => sd.ImplementationType == typeof(ScopeAwareHttpClientFactory)))
    {
        // Override default IHttpClientFactory registration
        builder.Services.AddTransient<IHttpClientFactory, ScopeAwareHttpClientFactory>();
    }

    builder.Services.Configure<ScopeAwareHttpClientFactoryOptions>(
        builder.Name, options => options.HttpHandlerType = typeof(THandler));

    return builder;
}

Para saber mais, consulte o exemplo completo.

Confira também