Partilhar via


Suporte de DI com chave no IHttpClientFactory

Neste artigo, você aprenderá a integrar IHttpClientFactory com Keyed Services.

Keyed Services (também chamado de Keyed DI) é um recurso de injeção de dependência (DI) que permite operar convenientemente com várias implementações de um único serviço. Após o registro, você pode associar diferentes chaves de serviço com as implementações específicas. Em tempo de execução, esta chave é usada na pesquisa em combinação com um tipo de serviço, o que significa que pode recuperar uma implementação específica passando a chave correspondente. Para obter mais informações sobre Serviços Marcados e DI (Injeção de Dependência) em geral, consulte injeção de dependência do .NET.

Para obter uma visão geral sobre como usar IHttpClientFactory em seu aplicativo .NET, consulte IHttpClientFactory com .NET.

Contexto geral

IHttpClientFactory e instâncias de HttpClient nomeadas, sem surpresa, alinham-se bem com a ideia de Keyed Services. Historicamente, entre outras coisas, IHttpClientFactory foi uma maneira de superar esse recurso de DI há muito ausente. Mas os clientes nomeados simples exigem que você obtenha, armazene e consulte a instância IHttpClientFactory — em vez de injetar um HttpClientconfigurado —, o que pode ser inconveniente. Embora os clientes digitados tentem simplificar essa parte, ela vem com um problema: os clientes digitados são fáceis de mal configurados e uso indevido, e a infraestrutura de suporte também pode ser uma sobrecarga tangível em certos cenários (por exemplo, em plataformas móveis).

A partir do .NET 9 (versão de pacotesMicrosoft.Extensions.Http e Microsoft.Extensions.DependencyInjection9.0.0+), IHttpClientFactory pode utilizar o Keyed DI diretamente, introduzindo uma nova "abordagem Keyed DI" (em oposição às abordagens "Named" e "Typed"). A "abordagem de DI com chave" associa os registros de HttpClient, que são convenientes e altamente configuráveis, com a injeção direta das instâncias específicas configuradas de HttpClient.

Utilização Básica

A partir do .NET 9, você precisa aceitar ao recurso chamando o método de extensão AddAsKeyed. Se aceito, o cliente nomeado que aplica a configuração é adicionado ao contêiner DI como um serviço de HttpClient com chave, usando o nome do cliente como uma chave de serviço, para que você possa usar as APIs padrão de Serviços com chave (por exemplo, FromKeyedServicesAttribute) para obter as instâncias de HttpClient nomeadas desejadas (criadas e configuradas por IHttpClientFactory). Por padrão, os clientes são registrados com escopo ciclo de vida.

O código a seguir ilustra a integração entre IHttpClientFactory, Keyed DI e ASP.NET Core 9.0 Minimal APIs:

var builder = WebApplication.CreateBuilder(args);

// --- (1) Registration ---
builder.Services.AddHttpClient("github", c =>
    {
        c.BaseAddress = new Uri("https://api.github.com/");
        c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
        c.DefaultRequestHeaders.Add("User-Agent", "dotnet");
    })
    .AddAsKeyed(); // Add HttpClient as a Keyed Scoped service for key="github"

var app = builder.Build();

// --- (2) Obtaining HttpClient instance ---
// Directly inject the Keyed HttpClient by its name
app.MapGet("/", ([FromKeyedServices("github")] HttpClient httpClient) =>
    // --- (3) Using HttpClient instance ---
    httpClient.GetFromJsonAsync<Repo>("/repos/dotnet/runtime"));

app.Run();

record Repo(string Name, string Url);

Resposta do parâmetro de avaliação:

> ~  curl http://localhost:5000/
{"name":"runtime","url":"https://api.github.com/repos/dotnet/runtime"}

No exemplo, o HttpClient configurado é injetado no gestor de pedidos através da infraestrutura padrão de DI com chaves, que está integrada na ligação de parâmetros do ASP.NET Core. Para obter mais informações sobre Serviços com chave no ASP.NET Core, consulte injeção de dependência no ASP.NET Core.

Comparação de abordagens chaveadas, nomeadas e digitadas

Considere apenas o código relacionado ao IHttpClientFactorydo exemplo de Uso Básico .

services.AddHttpClient("github", /* ... */).AddAsKeyed();                // (1)

app.MapGet("/", ([FromKeyedServices("github")] HttpClient httpClient) => // (2)
    //httpClient.Get....                                                 // (3)

Este trecho de código ilustra como o registro (1), obtendo a instância HttpClient configurada (2)e usando a instância de cliente obtida conforme necessário (3) pode parecer ao usar a abordagem Keyed DI.

Compare como as mesmas etapas são alcançadas com as duas abordagens "mais antigas".

Primeiro, com a abordagem Nomeado :

services.AddHttpClient("github", /* ... */);                          // (1)

app.MapGet("/github", (IHttpClientFactory httpClientFactory) =>
{
    HttpClient httpClient = httpClientFactory.CreateClient("github"); // (2)
    //return httpClient.Get....                                       // (3)
});

Em segundo lugar, com a abordagem Digitada :

services.AddHttpClient<GitHubClient>(/* ... */);          // (1)

app.MapGet("/github", (GitHubClient gitHubClient) =>
    gitHubClient.GetRepoAsync());

public class GitHubClient(HttpClient httpClient)          // (2)
{
    private readonly HttpClient _httpClient = httpClient;

    public Task<Repo> GetRepoAsync() =>
        //_httpClient.Get....                             // (3)
}

Das três, a abordagem Keyed DI oferece a maneira mais sucinta de alcançar o mesmo comportamento.

Validação de contêiner DI integrada

Se você habilitou o registro com chave para um cliente nomeado específico, poderá acessá-lo com quaisquer APIs DI com chave existentes. Mas se você erroneamente tentar usar um nome que ainda não está habilitado, você obtém a exceção padrão Keyed DI:

services.AddHttpClient("keyed").AddAsKeyed();
services.AddHttpClient("not-keyed");

provider.GetRequiredKeyedService<HttpClient>("keyed"); // OK

// Throws: No service for type 'System.Net.Http.HttpClient' has been registered.
provider.GetRequiredKeyedService<HttpClient>("not-keyed");

Além disso, o ciclo de vida dos clientes com escopo pode ajudar a detectar casos de dependências cativas.

services.AddHttpClient("scoped").AddAsKeyed();
services.AddSingleton<CapturingSingleton>();

// Throws: Cannot resolve scoped service 'System.Net.Http.HttpClient' from root provider.
rootProvider.GetRequiredKeyedService<HttpClient>("scoped");

using var scope = provider.CreateScope();
scope.ServiceProvider.GetRequiredKeyedService<HttpClient>("scoped"); // OK

// Throws: Cannot consume scoped service 'System.Net.Http.HttpClient' from singleton 'CapturingSingleton'.
public class CapturingSingleton([FromKeyedServices("scoped")] HttpClient httpClient)
//{ ...

Seleção do tempo de vida do serviço

Por padrão, AddAsKeyed() registra HttpClient como um serviço de com chave escopo definido. Você também pode especificar explicitamente o tempo de vida passando o parâmetro ServiceLifetime para o método AddAsKeyed():

services.AddHttpClient("explicit-scoped")
    .AddAsKeyed(ServiceLifetime.Scoped);

services.AddHttpClient("singleton")
    .AddAsKeyed(ServiceLifetime.Singleton);

Se você chamar AddAsKeyed() dentro de um registro de cliente digitado, somente o cliente nomeado subjacente será registrado como chaveado. O próprio cliente digitado continua a ser registrado como um serviço transitório simples.

Evite vazamentos de memória transitórios no HttpClient

Importante

HttpClient é IDisposable, por isso recomendamos vivamente evitar tempo de vida transitório para instâncias de HttpClient chaveado.

Registrar o cliente como um serviço Keyed Transient leva a que as instâncias HttpClient e HttpMessageHandler sejam capturadas pelo contentor DI, tal como ambos implementam IDisposable. Isso podem resultar em vazamentos de memória se o cliente for resolvido várias vezes nos serviços do tipo Singleton.

Evitar a dependência cativa

Importante

Se HttpClient estiver registado:

  • como um Keyed Singleton, -OR-
  • como um com escopo chaveado ou transitório e injetado dentro de um aplicação de longa duração (mais de HandlerLifetime), ou –
  • como um transitório com chave, e injetado em um serviço de Singleton,

—a instância HttpClient torna-se cativa e provavelmente sobreviverá ao seu tempo de vida HandlerLifetimeesperado. IHttpClientFactory não tem controlo sobre clientes cativos, eles NÃO podem participar na rotação do gestor e isto pode resultar em a perda das alterações ao DNS. Um problema semelhante já existe para clientes tipados, que são registrados como serviços transitórios.

Nos casos em que a longevidade do cliente não pode ser evitada – ou se é conscientemente desejada, por exemplo, para um Keyed Singleton – é aconselhável alavancar SocketsHttpHandler definindo PooledConnectionLifetime a um valor razoável.

services.AddHttpClient("shared")
    .AddAsKeyed(ServiceLifetime.Singleton) // explicit singleton
    .UseSocketsHttpHandler((h, _) => h.PooledConnectionLifetime = TimeSpan.FromMinutes(2))
    .SetHandlerLifetime(Timeout.InfiniteTimeSpan); // disable rotation
services.AddSingleton<MySingleton>();

public class MySingleton([FromKeyedServices("shared")] HttpClient shared) // { ...

Cuidado com a incompatibilidade de escopo

Embora o ciclo de vida de Scoped seja muito menos problemático para os Named HttpClient(em comparação com as armadilhas de Singleton e Transient), ele tem suas próprias particularidades.

Importante

O tempo de vida específico de uma instância HttpClient está vinculado, conforme esperado, ao escopo normal da aplicação (por exemplo, escopo de pedido recebido) de onde foi resolvido. No entanto, isto NÃO se aplica à cadeia de manipulador de mensagens subjacente, que ainda é gerida pelo IHttpClientFactory, da mesma forma que é para os clientes Named criados diretamente pela fábrica. HttpClients com o mesmo nome, mas resolvidos (dentro de um período de tempo HandlerLifetime) em dois âmbitos diferentes (por exemplo, duas solicitações simultâneas para o mesmo endpoint), podem reutilizar a mesmaHttpMessageHandler instância. Essa instância, por sua vez, tem seu próprio escopo separado, conforme ilustrado no Escopos do manipulador de mensagens.

Observação

O problema de incompatibilidade de escopo é desagradável, existe há muito tempo e, no .NET 9, ainda permanece não resolvido . A partir de um serviço injetado por meio da infra DI regular, você esperaria que todas as dependências fossem satisfeitas a partir do mesmo escopo, mas para as instâncias de HttpClient com escopo chaveado, infelizmente não é o caso.

Cadeia do manipulador de mensagens com chave

Para alguns cenários avançados, poderá querer aceder diretamente à cadeia HttpMessageHandler, em vez de a um objeto HttpClient. IHttpClientFactory fornece IHttpMessageHandlerFactory interface para criar os manipuladores; e se você ativar o DI com chave, não apenas HttpClient, mas também a respetiva cadeia de HttpMessageHandler está registrada como um serviço com chave:

services.AddHttpClient("keyed-handler").AddAsKeyed();

var handler = provider.GetRequiredKeyedService<HttpMessageHandler>("keyed-handler");
var invoker = new HttpMessageInvoker(handler, disposeHandler: false);

Como: Mudar da abordagem digitada para DI com chave

Observação

Atualmente, recomendamos o uso da abordagem Keyed DI em vez de clientes tipados.

Uma alteração mínima de um cliente Typed existente para uma dependência Keyed pode ser feita da seguinte forma:

- services.AddHttpClient<Service>(         // (1) Typed client
+ services.AddHttpClient(nameof(Service),  // (1) Named client
      c => { /* ... */ }                   // HttpClient configuration
  //).Configure....
- );
+ ).AddAsKeyed();                          // (1) + Keyed DI opt-in

+ services.AddTransient<Service>();        // (1) Plain Transient service

  public class Service(
-                                          // (2) "Hidden" Named dependency
+     [FromKeyedServices(nameof(Service))] // (2) Explicit Keyed dependency
      HttpClient httpClient) // { ...

No exemplo:

  1. O registo do cliente Especificado Service é dividido em:
    • Um registo de um cliente designado nameof(Service) com uma configuração idêntica a HttpClient e um opt-in para Keyed DI.
    • Serviço transitório simples Service.
  2. HttpClient dependência no Service está explicitamente vinculada a um Serviço com chave definida nameof(Service).

O nome não precisa ser nameof(Service), mas o exemplo visava minimizar as mudanças comportamentais. Internamente, os clientes tipados usam clientes nomeados e, por padrão, esses clientes nomeados "ocultos" adotam o nome do tipo do cliente tipado vinculado. Neste caso, o nome "oculto" foi nameof(Service), então o exemplo o preservou.

Tecnicamente, o exemplo "desempacota" o cliente Tipado, de modo que o cliente nomeado que antes estava "oculto" se torna "exposto," e a dependência é satisfeita através da infraestrutura DI chaveada em vez da infraestrutura do cliente Tipado.

Como: Optar por DI com chave por padrão

Você não precisa ligar para o AddAsKeyed para cada cliente. Pode optar facilmente por uma configuração "global" (para qualquer nome de cliente) através do ConfigureHttpClientDefaults. Do perspetiva dos Keyed Services, isso resulta no registo de KeyedService.AnyKey.

services.ConfigureHttpClientDefaults(b => b.AddAsKeyed());

services.AddHttpClient("first", /* ... */);
services.AddHttpClient("second", /* ... */);
services.AddHttpClient("third", /* ... */);

public class MyController(
    [FromKeyedServices("first")] HttpClient first,
    [FromKeyedServices("second")] HttpClient second,
    [FromKeyedServices("third")] HttpClient third)
//{ ...

Cuidado com clientes "desconhecidos"

Observação

KeyedService.AnyKey registros definem um mapeamento de qualquer valor de chave para alguma instância de serviço. No entanto, como resultado, a validação de contêiner não se aplica e um valor de chave incorreto silenciosamente leva a uma instância errada sendo injetada.

Importante

Para clientes Keyed HttpClient , um erro no nome do cliente pode resultar na injeção errada de um cliente "desconhecido" — ou seja, um cliente cujo nome nunca foi registado.

O mesmo é verdadeiro para os clientes nomeados simples: IHttpClientFactory não exige que o nome do cliente seja explicitamente registrado (alinhando-se com a maneira como o padrão Opções funciona). A fábrica fornece umaHttpClient não configurada — ou, mais precisamente, configurada por padrão — para qualquer nome desconhecido.

Observação

Por isso, é importante ter em mente: a abordagem "Keyed by default" abrange não só todos os clientes registadosHttpClient , mas todos os clientes que IHttpClientFactoryconseguem criar.

services.ConfigureHttpClientDefaults(b => b.AddAsKeyed());
services.AddHttpClient("known", /* ... */);

provider.GetRequiredKeyedService<HttpClient>("known");   // OK
provider.GetRequiredKeyedService<HttpClient>("unknown"); // OK (unconfigured instance)

Considerações sobre a estratégia "Opt-in"

Mesmo que o opt-in "global" seja um one-liner, é lamentável que o recurso ainda o exija, em vez de apenas funcionar "fora da caixa". Para obter o contexto completo e o raciocínio dessa decisão, consulte dotnet/runtime#89755 e dotnet/runtime#104943. Em suma, o impedimento principal para "ativo por defeito" é a "controvérsia" de ServiceLifetime: dado o estado atual (9.0.0) das implementações de DI e IHttpClientFactory, não há um único ServiceLifetime que seja razoavelmente seguro para todos os HttpClientem todas as situações possíveis. Existe, no entanto, a intenção de abordar as ressalvas nas próximas versões e mudar a estratégia de "opt-in" para "opt-out".

Como: Desativar o registo com chave

Você pode desativar explicitamente o DI com chave para HttpClients chamando o método de extensão RemoveAsKeyed, por nome de cliente:

services.ConfigureHttpClientDefaults(b => b.AddAsKeyed());      // opt IN by default
services.AddHttpClient("keyed", /* ... */);
services.AddHttpClient("not-keyed", /* ... */).RemoveAsKeyed(); // opt OUT per name

provider.GetRequiredKeyedService<HttpClient>("keyed");     // OK
provider.GetRequiredKeyedService<HttpClient>("not-keyed"); // Throws: No service for type 'System.Net.Http.HttpClient' has been registered.
provider.GetRequiredKeyedService<HttpClient>("unknown");   // OK (unconfigured instance)

Ou "globalmente" com ConfigureHttpClientDefaults:

services.ConfigureHttpClientDefaults(b => b.RemoveAsKeyed()); // opt OUT by default
services.AddHttpClient("keyed", /* ... */).AddAsKeyed();      // opt IN per name
services.AddHttpClient("not-keyed", /* ... */);

provider.GetRequiredKeyedService<HttpClient>("keyed");     // OK
provider.GetRequiredKeyedService<HttpClient>("not-keyed"); // Throws: No service for type 'System.Net.Http.HttpClient' has been registered.
provider.GetRequiredKeyedService<HttpClient>("unknown");   // Throws: No service for type 'System.Net.Http.HttpClient' has been registered.

Ordem de precedência

Se forem convocados juntos ou qualquer um deles mais de uma vez, AddAsKeyed() e RemoveAsKeyed() geralmente seguem as regras de configurações de IHttpClientFactory e registos DI:

  1. Se for chamado para o mesmo nome, vence a última configuração: o tempo de vida do último AddAsKeyed() é usado para criar o registro com chave (a menos que RemoveAsKeyed() tenha sido chamado por último, caso em que o nome é excluído).
  2. Se usado apenas dentro ConfigureHttpClientDefaults, a última configuração vence.
  3. Se ConfigureHttpClientDefaults e um nome específico do cliente forem usados, todos os padrões são considerados como ocorrendo antes de todas as configurações por nome. Assim, os padrões podem ser desconsiderados, e a última das configurações por nome vence.

Ver também