Compartir vía


Compatibilidad de DI con claves en IHttpClientFactory

En este artículo, aprenderá a integrar IHttpClientFactory con Keyed Services.

Los servicios con claves (que también se denominan DI con claves) son una característica de inserción de dependencias (DI) que permite operar cómodamente con varias implementaciones de un único servicio. Tras el registro, puede asociar diferentes claves de servicio con las implementaciones específicas. En tiempo de ejecución, esta clave se usa en la búsqueda en combinación con un tipo de servicio, lo que significa que puede recuperar una implementación específica pasando la clave coincidente. Para obtener más información sobre los servicios con claves y DI en general, consulte Inserción de dependencias de .NET.

Para obtener información general sobre cómo usar IHttpClientFactory en la aplicación .NET, consulte IHttpClientFactory con .NET.

Fondo

Las instancias de IHttpClientFactory y de HttpClient con nombre, como es de esperar, se ajustan bien a la idea de los servicios con claves. Históricamente, entre otras cosas, IHttpClientFactory era una forma de sustituir esta funcionalidad de inserción de dependencias que estuvo ausente durante tanto tiempo. Pero los clientes con nombre simples requieren que obtenga, almacene y consulte la instancia de IHttpClientFactory, en lugar de insertar un HttpClientconfigurado, lo que podría no ser práctico. Aunque los clientes con tipo intentan simplificar esa parte, aquí hay un problema: los clientes con tipo son fáciles de configurar incorrectamente y usar mal, y la infraestructura auxiliar también podría ser una sobrecarga tangible en determinados escenarios (por ejemplo, en plataformas móviles).

A partir de .NET 9 (paquetes Microsoft.Extensions.Http y Microsoft.Extensions.DependencyInjection de la versión 9.0.0+), IHttpClientFactory puede utilizar directamente la DI con claves, lo que introduce un nuevo "enfoque de DI con claves" (en lugar de enfoques "con nombre" y "con tipo"). El "enfoque DI con claves" asocia los registros de HttpClient, que son convenientes y altamente configurables, con una sencilla inyección de instancias específicas de HttpClient ya configuradas.

Uso básico

A partir de .NET 9, debe activar la característica llamando al método de extensión AddAsKeyed. Si la activa, el cliente con nombre que aplica la configuración se añade al contenedor de DI como un servicio de HttpClient con claves, utilizando el nombre del cliente como clave de servicio, por lo que puede usar las API estándar de los servicios con claves (por ejemplo, FromKeyedServicesAttribute) para obtener las instancias de HttpClientcon nombre (creadas y configuradas por IHttpClientFactory). De forma predeterminada, los clientes se registran con una duración con ámbito.

En el código siguiente se muestra la integración entre IHttpClientFactory, DI con claves y las API mínimas de ASP.NET Core 9.0:

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

Respuesta del punto de conexión:

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

En el ejemplo, el HttpClient configurado se inserta en el controlador de solicitudes a través de la infraestructura del DI con claves estándar, que está integrado en la vinculación de parámetros de ASP.NET Core. Para más información sobre los servicios con claves en ASP.NET Core, consulte Inserción de dependencias en ASP.NET Core.

Comparación de los enfoques por clave, por nombre y por tipo

Tenga en cuenta únicamente el código relacionado con IHttpClientFactorydel ejemplo de uso básico de :

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

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

Este fragmento de código muestra cómo el registro (1), la obtención de la instancia configurada HttpClient(2)y el uso de la instancia de cliente obtenida según sea necesario (3) puede aparecer al usar el enfoque de DI con claves .

Compare cómo se logran los mismos pasos con los dos enfoques "más antiguos".

En primer lugar, con el enfoque con nombre:

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

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

En segundo lugar, con el enfoque con tipo:

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

De las tres, el enfoque de DI con claves es la manera más concisa de conseguir el mismo comportamiento.

Validación integrada de contenedores de DI

Si ha habilitado el registro con claves para un cliente con nombre específico, puede acceder a él con cualquier API de DI con clave existente. Pero si por error intenta usar un nombre que aún no está habilitado, obtendrá la excepción de DI con clave estándar.

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");

Además, la duración con ámbito de los clientes puede ayudar a detectar casos de dependencias cautivas:

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)
//{ ...

Selección de duración del servicio

De forma predeterminada, AddAsKeyed() registra HttpClient como un servicio Keyed de ámbito. También puede especificar explícitamente la duración pasando el parámetro ServiceLifetime al método AddAsKeyed():

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

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

Si llama a AddAsKeyed() dentro de un registro de cliente con tipo, solo el cliente con nombre subyacente se registra como con clave. El Typed client en sí sigue registrándose como un servicio transitorio simple.

Evitar la fuga transitoria de memoria de HttpClient

Importante

HttpClient es IDisposable, por lo que se recomienda encarecidamente evitar la duración transitoria en las instancias de HttpClient con claves.

El registro del cliente como servicio transitorio con claves hace que el contenedor de DI capture las instancias de HttpClient y HttpMessageHandler, ya que ambas implementan IDisposable. Esto puede provocar pérdidas de memoria si el cliente se resuelve varias veces dentro de los servicios singleton.

Evitar dependencias cautivas

Importante

Si se registra HttpClient, ya sea:

  • como singleton con claves O
  • como con ámbito o transitorio con claves y se inserta dentro de un ámbito de aplicación de larga duración (más de HandlerLifetime), O
  • como un transitorio con claves y se inserta en un servicio singleton,

la instancia HttpClient se convierte en cautiva y probablemente sobreviva a su HandlerLifetime esperada. IHttpClientFactory no tiene ningún control sobre los clientes cautivos; estos no pueden participar en la rotación del controlador, y esto puede provocar la pérdida de cambios de DNS. Se produce un problema similar para los clientes con tipo, que se registran como servicios transitorios.

En los casos en los que no se puede evitar la longevidad del cliente, o si se desea utilizar conscientemente, por ejemplo, para un singleton con claves, se recomienda utilizar SocketsHttpHandler estableciendo PooledConnectionLifetime en un valor razonable.

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) // { ...

Tenga cuidado con la falta de coincidencia del ámbito

Aunque la duración de ámbito es mucho menos problemática para los HttpClientcon nombre (en comparación con los inconvenientes de singleton y transitorios), tiene otros problemas.

Importante

La duración con ámbito con claves de una instancia específica de HttpClient está vinculada, como era de esperar, al ámbito de aplicación "normal" (por ejemplo, el ámbito de solicitud entrante) desde el que se resolvió. Sin embargo, no se aplica a la cadena del controlador de mensajes subyacente, que todavía administra IHttpClientFactory, de la misma manera que para los clientes con nombre que se crean directamente desde la fábrica. Los HttpClient con el mismo nombre, pero resuelto (dentro de un período deHandlerLifetime) en dos ámbitos diferentes (por ejemplo, dos solicitudes simultáneas al mismo punto de conexión), pueden reutilizar la mismaHttpMessageHandler instancia. Esa instancia, a su vez, tiene su propio ámbito independiente, como se muestra en el controlador de mensajes .

Nota

El problema de la falta de coincidencia del ámbito es desagradable y ha existido durante mucho tiempo, y en .NET 9 sigue estando sin resolver. Desde un servicio insertado a través de la infraestructura de DI habitual, se espera que todas las dependencias se cumplan desde el mismo ámbito. Sin embargo, para las instancias de HttpClient con ámbito y con claves, lamentablemente no es así.

Cadena de gestión de mensajes con clave

En algunos escenarios avanzados, es posible que desee acceder directamente a la cadena HttpMessageHandler, en vez de a un objeto HttpClient. IHttpClientFactory proporciona la interfaz IHttpMessageHandlerFactory para crear los controladores. Si habilita Keyed DI, no solo HttpClient, sino también la cadena respectiva de HttpMessageHandler se registra como un servicio con clave.

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

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

Cómo cambiar del enfoque con tipo a DI con claves

Nota

Actualmente recomendamos usar DI con claves en lugar de clientes con tipo.

Un cambio mínimo de un cliente tipado existente a una dependencia con clave puede verse de la siguiente manera:

- 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) // { ...

En el ejemplo:

  1. El registro del cliente con tipo Service se divide en:
    • Un registro de un cliente con nombre nameof(Service) con la misma configuración de HttpClient y la activación de DI con claves; y
    • Servicio transitorio simple Service.
  2. La dependencia HttpClient en Service está explícitamente vinculada a un servicio con clave nameof(Service).

El nombre no tiene que ser nameof(Service), pero el ejemplo pretende minimizar los cambios de comportamiento. Internamente, los clientes con tipo utilizan clientes con nombre y, de manera predeterminada, estos clientes con nombre "ocultos" usan el nombre del tipo del cliente con tipo vinculado. En este caso, el nombre "oculto" era nameof(Service), por lo que el ejemplo lo conservó.

Técnicamente, el ejemplo "desencapsula" el cliente con tipo, de modo que el cliente con nombre previamente "oculto" queda "expuesto", y la dependencia se satisface a través de la infraestructura de DI con claves en lugar de la infraestructura del cliente con tipo.

Cómo activar DI con claves de forma predeterminada

No necesitas llamar a AddAsKeyed para cada cliente individualmente; puedes optar fácilmente por el modo "global" (para cualquier nombre de cliente) a través de ConfigureHttpClientDefaults. Desde la perspectiva de los servicios con claves, se produce el registro 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)
//{ ...

Tenga cuidado con los clientes "desconocidos"

Nota

Los registros de KeyedService.AnyKey definen una asignación de cualquier valor de clave a alguna instancia de servicio. Sin embargo, como resultado, la validación del contenedor no se aplica y un valor de clave erróneo hace que, silenciosamente, se inyecte una instancia equivocada.

Importante

Para keyed HttpClients, un error en el nombre de cliente puede dar lugar a insertar erróneamente un cliente "desconocido", es decir, un cliente cuyo nombre nunca se registró.

Lo mismo sucede con los clientes con nombre simples: IHttpClientFactory no requiere que el nombre del cliente se registre explícitamente (lo que se ajusta a la forma en que funciona el Patrón de opciones). La fábrica proporciona un HttpClient sin configurar o, para ser más precisos, configurado de forma predeterminada, para cualquier nombre desconocido.

Nota

Por lo tanto, es importante tener en cuenta que el enfoque de "con claves de manera predeterminada" abarca no solo todos los clientes registradosHttpClient, sino también todos los clientes que IHttpClientFactory es capaz de crear.

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

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

Consideraciones de la estrategia de suscripción voluntaria

Aunque la activación "global" sea rápida, es una pena que aún sea necesario hacerlo, en lugar de funcionar sin ninguna configuración previa. Para obtener el contexto completo y el razonamiento sobre esa decisión, consulte dotnet/runtime#89755 y dotnet/runtime#104943. En resumen, el principal impedimento para que esté "activada de forma predeterminada" es la "controversia" de ServiceLifetime: dado el estado actual (9.0.0) de las implementaciones de DI y IHttpClientFactory, no hay un único ServiceLifetime que sea razonablemente seguro para todos los HttpClient en todas las situaciones posibles. Sin embargo, hay una intención de abordar estos problemas en las próximas versiones y cambiar la estrategia de "activación" a "desactivación".

Cómo: No participar en el registro con claves

Puede desactivar explícitamente DI con claves para HttpClient mediante una llamada al método de extensión RemoveAsKeyed, ya sea por el nombre 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)

O "globalmente" con 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.

Orden de prioridad

Si se llaman juntos o cualquiera de ellos más de una vez, AddAsKeyed() y RemoveAsKeyed() suelen seguir las reglas de las configuraciones de IHttpClientFactory y los registros de DI:

  1. Si se llama para el mismo nombre, la última configuración es la que se aplica: la duración de la última AddAsKeyed() se usa para crear el registro con claves (a menos que se hubiera llamado a RemoveAsKeyed() la última vez, en cuyo caso se excluye el nombre).
  2. Si solo se usa dentro de ConfigureHttpClientDefaults, se aplica el último ajuste.
  3. Si se utilizaron tanto ConfigureHttpClientDefaults como el nombre específico del cliente, se considera que todos los valores predeterminados eran anteriores a todas las configuraciones individuales. Por lo tanto, los valores predeterminados se pueden ignorar y se aplica la última configuración por nombre.

Consulte también