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 HttpClient
configurado, 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 HttpClient
con 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 IHttpClientFactory
del 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 HttpClient
con 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:
- El registro del cliente con tipo
Service
se divide en:- Un registro de un cliente con nombre
nameof(Service)
con la misma configuración deHttpClient
y la activación de DI con claves; y - Servicio transitorio simple
Service
.
- Un registro de un cliente con nombre
- La dependencia
HttpClient
enService
está explícitamente vinculada a un servicio con clavenameof(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 HttpClient
s, 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:
- 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 aRemoveAsKeyed()
la última vez, en cuyo caso se excluye el nombre). - Si solo se usa dentro de
ConfigureHttpClientDefaults
, se aplica el último ajuste. - 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.