Partilhar via


IHttpClientFactory com .NET

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

Com princípios modernos de desenvolvimento de aplicativos orientando as práticas recomendadas, o IHttpClientFactory serve como uma abstração de fábrica que pode criar HttpClient instâncias com configurações personalizadas. IHttpClientFactory foi introduzido no .NET Core 2.1. Trabalhos .NET comuns baseados em HTTP podem com facilidade tirar partido de middleware de terceiros resiliente e de gestão de falhas transitórias.

Advertência

Se a sua aplicação requer cookies, é recomendável evitar o uso de IHttpClientFactory. O agrupamento das HttpMessageHandler instâncias resulta no compartilhamento de CookieContainer objetos. O compartilhamento inesperado CookieContainer pode causar vazamento de cookies entre partes não relacionadas da aplicação. Além disso, quando HandlerLifetime expira, o gestor é reciclado, o que significa que todos os cookies armazenados no seu CookieContainer são perdidos. Para obter formas alternativas de gerenciar clientes, consulte Diretrizes para usar clientes HTTP.

Importante

O gerenciamento do tempo de vida das HttpClient instâncias criadas por 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 configurados. Para obter mais informações, consulte a seção Gerenciamento do tempo de vida do HttpClient e Diretrizes para o uso de clientes HTTP.

O IHttpClientFactory tipo

Todo o código-fonte de exemplo fornecido neste artigo requer a instalação do Microsoft.Extensions.Http pacote NuGet. Além disso, os exemplos de código demonstram o uso de pedidos HTTP GET para recuperar objetos de utilizador Todo da API {JSON} Placeholder gratuita.

Ao chamar qualquer um dos métodos de extensão de AddHttpClient, está-se a adicionar IHttpClientFactory e os serviços relacionados ao IServiceCollection. O IHttpClientFactory tipo 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 HttpClient .
  • Codifica o conceito de middleware de saída através da delegação de manipuladores em HttpClient.
  • Fornece métodos de extensão para middleware baseado em Polly para tirar proveito da delegação de manipuladores no HttpClient.
  • Gerencia o cache e o tempo de vida das instâncias subjacentes HttpClientHandler . A gestão automática evita problemas comuns do Sistema de Nomes de Domínio (DNS) que ocorrem ao gerir manualmente a duração de vida de HttpClient.
  • Adiciona uma experiência de registro configurável (via ILogger) para todas as solicitações enviadas através de clientes criados pela fábrica.

Padrões de consumo

Existem várias maneiras como IHttpClientFactory pode ser usado numa aplicação.

A melhor abordagem depende dos requisitos do aplicativo.

Utilização básica

Para registar o IHttpClientFactory, ligue para 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 de construtor com DI. O código a seguir usa IHttpClientFactory para criar uma HttpClient instância:

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
        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 tem impacto na forma como HttpClient é utilizado. Em locais onde instâncias de HttpClient são criadas numa aplicação existente, substitua essas ocorrências por chamadas a CreateClient.

Clientes nomeados

Clientes nomeados são uma boa escolha quando:

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

A configuração para um HttpClient nomeado pode ser especificada durante o registo 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 https://jsonplaceholder.typicode.com/.
  • Um "User-Agent" cabeçalho.

Você pode usar a configuração para especificar nomes de clientes HTTP, o que é útil para evitar nomes incorretos 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, consulte Configuração no .NET.

Criar cliente

Cada vez que CreateClient é chamado:

  • Uma nova instância do HttpClient é criada.
  • Chama-se a ação de configuração.

Para criar um cliente nomeado, passe seu nome 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"];
        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 de host. O código pode passar apenas o caminho, uma vez que o endereço base configurado para o cliente é usado.

Clientes tipificados

Clientes tipificados

  • Forneça os mesmos recursos que os clientes nomeados sem a necessidade de usar cadeias de caracteres como chaves.
  • Ofereça IntelliSense e ajuda do compilador quando os clientes estiverem a consumir os serviços.
  • Forneça um único local para configurar e interagir com um determinado HttpClient. Por exemplo, um único cliente tipificado pode ser usado:
    • Para um único endpoint de backend.
    • Para encapsular toda a lógica que lida com o endpoint.
  • Trabalhe com DI e pode ser injetado quando necessário no aplicativo.

Um cliente tipado aceita um HttpClient parâmetro no 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)
{
    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 [];
    }
}

No código anterior:

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

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

O código a seguir chama AddHttpClient para registrar uma classe de cliente digitada:

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 digitado é registrado como transitório com DI. No código anterior, AddHttpClient registra-se TodoService como um serviço transitório. Este registo utiliza 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 o seu construtor.

Importante

Usar clientes digitados em serviços singleton pode ser perigoso. Para obter mais informações, consulte a seção Evitar clientes tipificados em serviços singleton.

Nota

Ao registrar um cliente tipado com o AddHttpClient<TClient> método, o TClient tipo deve ter um construtor que aceite um HttpClient como parâmetro. Além disso, o tipo TClient não deve ser registado separadamente no contêiner DI, pois isso fará com que o registo posterior substitua o anterior.

Clientes gerados

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

Considere o seguinte record tipo:

namespace Shared;

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

O exemplo a seguir depende do Refit.HttpClientFactory pacote NuGet 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 Task<Todo[]> instância.
  • Declara um Refit.GetAttribute atributo com o caminho e a cadeia de caracteres de consulta para a API externa.

Um cliente tipado pode ser adicionado, usando o 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 Refit.

Fazer solicitações POST, PUT e DELETE

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

  • POST
  • PUT
  • DELETE
  • PATCH

Para obter uma lista completa dos verbos HTTP suportados, consulte HttpMethod. Para obter mais informações sobre como fazer solicitações HTTP, consulte Enviar uma solicitação usando HttpClient.

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

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 Item parâmetro para JSON usando System.Text.Json. Isto 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 no corpo da solicitação HTTP.
  • Chamadas PostAsync para enviar o conteúdo JSON para a URL especificada. Esta é uma URL relativa que é adicionada ao HttpClient.BaseAddress.
  • Chama EnsureSuccessStatusCode para lançar uma exceção se o código de status da resposta não indicar sucesso.

HttpClient também suporta outros tipos de conteúdo. Por exemplo, MultipartContent e StreamContent. Para obter uma lista completa do conteúdo suportado, consulte HttpContent.

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

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 POST exemplo. O UpdateItemAsync método chama PutAsync em vez de PostAsync.

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

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

    httpResponse.EnsureSuccessStatusCode();
}

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

Para obter mais informações sobre a utilização de diferentes verbos HTTP com HttpClient, consulte HttpClient.

HttpClient gestão do tempo de vida

Uma nova HttpClient instância é retornada sempre que CreateClient é chamada no IHttpClientFactory. Uma HttpClientHandler instância é criada por nome de cliente. A fábrica gerencia o tempo de vida das HttpClientHandler instâncias.

IHttpClientFactory Armazena em cache as HttpClientHandler instâncias criadas pela fábrica para reduzir o consumo de recursos. Uma HttpClientHandler instância pode ser reutilizada do cache ao criar uma nova HttpClient instância se seu tempo de vida não tiver expirado.

O armazenamento em cache de manipuladores é desejável, pois cada manipulador normalmente gerencia seu próprio pool de conexões HTTP subjacente. Criar mais manipuladores do que o necessário pode resultar em exaustão do soquete e atrasos de conexão. Alguns manipuladores também mantêm as conexões abertas indefinidamente, o que pode impedir que o manipulador reaja às alterações de DNS.

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

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

Importante

HttpClient As instâncias criadas por IHttpClientFactory destinam-se a ser de curta duração.

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

  • A eliminação dessas instâncias HttpClientcriadas pela fábrica não levará ao esgotamento dos sockets, pois seu descarte não desencadeará o descarte do HttpMessageHandler. IHttpClientFactory rastreia e elimina os recursos usados para criar HttpClient instâncias, especificamente as HttpMessageHandler instâncias, assim que a sua vida útil expira e já não há mais nenhum HttpClient a usá-los.

Manter uma única HttpClient instância ativa por um longo período é um padrão comum que pode ser usado como uma alternativa ao IHttpClientFactory, no entanto, esse padrão requer configuração adicional, como PooledConnectionLifetime. Pode utilizar tanto clientes de longa duração com PooledConnectionLifetime, como clientes de curta duração criados por IHttpClientFactory. Para obter informações sobre qual estratégia usar em seu aplicativo, consulte Diretrizes para usar clientes HTTP.

Configura o HttpMessageHandler

Pode ser necessário controlar a configuração interna do HttpMessageHandler utilizado por um cliente.

Um IHttpClientBuilder é retornado ao adicionar clientes nomeados ou digitados. O método de extensão ConfigurePrimaryHttpMessageHandler pode ser usado para definir um delegado em IServiceCollection. O delegado é usado para criar e configurar o componente principal HttpMessageHandler usado por aquele cliente.

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

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

Configuração adicional

Existem 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 HttpClient nomeado.
AddTypedClient Configura a vinculação entre o TClient e o HttpClient designado associado ao IHttpClientBuilder.
ConfigureHttpClient Adiciona um delegado que será usado para configurar uma configuração nomeada HttpClient.
ConfigurePrimaryHttpMessageHandler Configura o componente principal HttpMessageHandler do contentor de injeção de dependência para um componente nomeado HttpClient.
RedactLoggedHeaders Define a coleção de nomes de cabeçalho HTTP para os quais os valores devem ser editados antes do registro.
SetHandlerLifetime Define o período de tempo durante o qual uma HttpMessageHandler instância pode ser reutilizada. Cada cliente nomeado pode ter seu próprio valor de vida útil do manipulador configurado.
UseSocketsHttpHandler Configura uma nova instância ou uma instância adicionada SocketsHttpHandler anteriormente do contentor de injeção de dependência para ser usada como um manipulador primário para um HttpClient nomeado. (Somente .NET 5+)

Usando IHttpClientFactory junto com SocketsHttpHandler

A implementação de SocketsHttpHandler foi adicionada no HttpMessageHandler .NET Core 2.1, que permite que PooledConnectionLifetime seja configurado. Esta 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, consulte Diretrizes para usar clientes HTTP.

No entanto, SocketsHttpHandler e IHttpClientFactory pode ser usado em conjunto melhorar a configurabilidade. Ao usar ambas as APIs, você se beneficia da configurabilidade em um nível baixo (por exemplo, usando LocalCertificateSelectionCallback para seleção dinâmica de certificados) e alto nível (por exemplo, aproveitando a integração DI e várias configurações de cliente).

Para usar ambas as APIs:

  1. Especifique SocketsHttpHandler como PrimaryHandler via ConfigurePrimaryHttpMessageHandler, ou UseSocketsHttpHandler (somente .NET 5+).
  2. SocketsHttpHandler.PooledConnectionLifetime Configurar com base no intervalo que o utilizador espera que o DNS seja atualizado; por exemplo, para um valor que estava anteriormente em HandlerLifetime.
  3. (Opcional) Uma vez que SocketsHttpHandler irá lidar com o pool de ligações e a reciclagem, a reciclagem do manipulador IHttpClientFactory ao nível já não é necessária. Você pode desativá-lo definindo HandlerLifetime como Timeout.InfiniteTimeSpan.
services.AddHttpClient(name)
    .UseSocketsHttpHandler((handler, _) =>
        handler.PooledConnectionLifetime = TimeSpan.FromMinutes(2)) // Recreate connection every 2 minutes
    .SetHandlerLifetime(Timeout.InfiniteTimeSpan); // Disable rotation, as it is handled by PooledConnectionLifetime

No exemplo acima, 2 minutos foram escolhidos arbitrariamente para fins de ilustração, alinhando-se a um valor padrão HandlerLifetime . Você deve escolher o valor com base na frequência esperada de DNS ou outras alterações de rede. Para obter mais informações, consulte a seção Comportamento do HttpClient DNS nas diretrizes e a seção Comentários na documentação da PooledConnectionLifetime API.

Evite clientes tipificados em serviços singleton

Ao usar a abordagem de cliente nomeado, é injetado IHttpClientFactory

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

Importante

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

Caso precise utilizar instâncias de HttpClient num serviço singleton, considere as seguintes opções:

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

Escopos do manipulador de mensagens em IHttpClientFactory

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

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

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

Se tiver necessidade de aceder a um escopo de DI de aplicativo a partir do seu próprio manipulador de mensagens, por exemplo, para autenticação, deverá encapsular a lógica com reconhecimento de escopo num transitório DelegatingHandler separado e encapsulá-la em torno de uma instância HttpMessageHandler do cache IHttpClientFactory. Para aceder à chamada do manipulador IHttpMessageHandlerFactory.CreateHandler para qualquer cliente nomeado registrado. Nesse caso, você mesmo criaria uma HttpClient instância usando o manipulador construído.

Diagrama mostrando como obter acesso aos escopos DI do aplicativo por meio de um manipulador de mensagens transitórias separado e IHttpMessageHandlerFactory

O exemplo a seguir mostra a criação de um HttpClient com um DelegatingHandler ciente do 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);

Uma abordagem alternativa adicional pode seguir com um método de extensão para registar um DelegatingHandler com reconhecimento de escopo e substituir o registo padrão de IHttpClientFactory por um serviço transitório com acesso ao escopo atual da aplicação.

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 obter mais informações, consulte o exemplo completo.

Evite depender de um gestor primário "padrão de fábrica"

Nesta seção, o termo "padrão de fábrica" Manipulador Primário refere-se ao Manipulador Primário que a implementação padrão IHttpClientFactory (ou, mais precisamente, a implementação padrão HttpMessageHandlerBuilder) atribui se não configurada de forma alguma.

Nota

O manipulador primário "padrão de fábrica" é um detalhe de implementação e está sujeito a alterações. ❌ EVITE depender de uma implementação específica que está sendo usada como um "padrão de fábrica" (por exemplo, HttpClientHandler).

Há casos em que precisa saber o tipo específico de um Manipulador Primário, especialmente se estiver a trabalhar numa biblioteca de classes. Ao mesmo tempo que preserva a configuração do utilizador final, pode querer atualizar, por exemplo, propriedades específicas de HttpClientHandler, como ClientCertificates, UseCookiese UseProxy. Pode ser tentador converter o Manipulador Primário em HttpClientHandler, o que funcionou enquantoHttpClientHandler era usado como o Manipulador Primário "padrão de fábrica". Mas, como qualquer código dependendo dos detalhes da implementação, essa solução alternativa é frágil e fadada a quebrar.

Em vez de confiar no Manipulador Primário "padrão de fábrica", você pode usáConfigureHttpClientDefaults para configurar uma instância do Manipulador Primário padrão no nível do aplicativo:

// Contract with the end-user: Only HttpClientHandler is supported.

// --- "Pre-configure" stage ---
// The default is fixed as HttpClientHandler to avoid depending on the "factory-default"
// Primary Handler.
services.ConfigureHttpClientDefaults(b =>
    b.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() { UseCookies = false }));

// --- "End-user" stage ---
// IHttpClientBuilder builder = services.AddHttpClient("test", /* ... */);
// ...

// --- "Post-configure" stage ---
// The code can rely on the contract, and cast to HttpClientHandler only.
builder.ConfigurePrimaryHttpMessageHandler((handler, provider) =>
    {
        if (handler is not HttpClientHandler h)
        {
            throw new InvalidOperationException("Only HttpClientHandler is supported");
        }

        h.ClientCertificates.Add(GetClientCert(provider, builder.Name));

        //X509Certificate2 GetClientCert(IServiceProvider p, string name) { ... }
    });

Como alternativa, você pode considerar verificar o tipo de manipulador primário e configurar as especificidades, como certificados de cliente, somente nos tipos de suporte conhecidos (provavelmente, HttpClientHandler e SocketsHttpHandler):

// --- "End-user" stage ---
// IHttpClientBuilder builder = services.AddHttpClient("test", /* ... */);
// ...

// --- "Post-configure" stage ---
// No contract is in place. Trying to configure main handler types supporting client
// certs, logging and skipping otherwise.
builder.ConfigurePrimaryHttpMessageHandler((handler, provider) =>
    {
        if (handler is HttpClientHandler h)
        {
            h.ClientCertificates.Add(GetClientCert(provider, builder.Name));
        }
        else if (handler is SocketsHttpHandler s)
        {
            s.SslOptions ??= new System.Net.Security.SslClientAuthenticationOptions();
            s.SslOptions.ClientCertificates ??= new X509CertificateCollection();
            s.SslOptions.ClientCertificates!.Add(GetClientCert(provider, builder.Name));
        }
        else
        {
            // Log warning
        }

        //X509Certificate2 GetClientCert(IServiceProvider p, string name) { ... }
    });

Consulte também