IHttpClientFactory con .NET

En este artículo verá cómo usar IHttpClientFactory para crear tipos HttpClient con varios aspectos básicos de .NET, como la inserción de dependencias (ID), los registros y la configuración. El tipo HttpClient se introdujo en .NET Framework 4.5, que se publicó en 2012. En otras palabras, ha existido durante un tiempo. HttpClient se usa para hacer solicitudes HTTP y controlar las respuestas HTTP de los recursos web identificados por un Uri. El protocolo HTTP compone la mayor parte de todo el tráfico de Internet.

Dado que los principios modernos de desarrollo de aplicaciones rigen los procedimientos recomendados, IHttpClientFactory actúa como una abstracción de fábrica que puede crear instancias de HttpClient con configuraciones personalizadas. IHttpClientFactory se presentó en .NET Core 2.1. Las cargas de trabajo de .NET comunes basadas en HTTP pueden aprovechar con facilidad el middleware de terceros resistente y con control de errores transitorios.

Nota

Si la aplicación requiere cookies, puede ser preferible evitar el uso de IHttpClientFactory en la aplicación. Para obtener formas alternativas de administrar clientes, consulte Directrices para usar clientes HTTP.

Importante

La administración de la duración de las instancias de HttpClient creadas por IHttpClientFactory es completamente diferente a la de las instancias creadas manualmente. Las estrategias son usar clientes de corta duración creados por IHttpClientFactory o clientes de larga duración con PooledConnectionLifetime configurado. Para obtener más información, consulte la sección Administración de la duración de HttpClient y Directrices para usar HttpClient.

El tipo IHttpClientFactory

Todo el código fuente de ejemplo de este artículo se basa en el paquete NuGet Microsoft.Extensions.Http. Además, se realizan solicitudes HTTP GET a la API gratuita {JSON} Placeholder para obtener objetos Todo de usuario.

Cuando llama a cualquiera de los métodos de extensión AddHttpClient, está agregando IHttpClientFactory y los servicios y relacionados a IServiceCollection. El tipo IHttpClientFactory ofrece las ventajas siguientes:

  • Expone la clase HttpClient como tipo listo para la inserción de dependencias.
  • Proporciona una ubicación central para denominar y configurar instancias de HttpClient lógicas.
  • Codifica el concepto de middleware de salida a través de la delegación de controladores en HttpClient.
  • Proporciona métodos de extensión para el middleware basado en Polly a fin de aprovechar los controladores de delegación en HttpClient.
  • Administra el almacenamiento en caché y la duración de las instancias de HttpClientHandler subyacentes. La administración automática evita los problemas comunes del Sistema de nombres de dominio (DNS) que se producen al administrar la duración de HttpClient de forma manual.
  • Agrega una experiencia de registro configurable (a través de ILogger) en todas las solicitudes enviadas a través de los clientes creados por Factory.

Patrones de consumo

IHttpClientFactory se puede usar de varias formas en una aplicación:

El mejor enfoque depende de los requisitos de la aplicación.

Uso básico

Para registrar IHttpClientFactory, llame a 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();

El consumo de servicios puede requerir IHttpClientFactory como parámetro de constructor con la inserción de dependencias. En el código siguiente se usa IHttpClientFactory para crear una instancia de 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 [];
    }
}

El uso de IHttpClientFactory como en el ejemplo anterior es una buena manera de refactorizar una aplicación existente. No tiene efecto alguno en la forma de usar HttpClient. En aquellos sitios de una aplicación existente en los que ya se hayan creado instancias de HttpClient, reemplace esas repeticiones por llamadas a CreateClient.

Clientes con nombre

Los clientes con nombre son una buena opción cuando:

  • La aplicación requiere muchos usos distintos de HttpClient.
  • Muchas instancias de HttpClient tienen otras configuraciones.

La configuración de un objeto HttpClient con nombre se puede realizar durante la fase de registro en 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");
    });

En el código anterior, el cliente está configurado con:

  • Un nombre que se extrae de la configuración en "TodoHttpClientName".
  • La dirección base. https://jsonplaceholder.typicode.com/.
  • Encabezado de "User-Agent".

Puede usar la configuración para especificar nombres de cliente HTTP, lo que resulta útil para evitar errores de nomenclatura de clientes al agregar y crear. En este ejemplo, el archivo appsettings.json se usa para configurar el nombre de cliente HTTP:

{
    "TodoHttpClientName": "JsonPlaceholderApi"
}

Es fácil ampliar esta configuración y almacenar más detalles sobre cómo quiere que funcione el cliente HTTP. Para obtener más información, vea Configuración en .NET.

Crear el cliente

Cada vez que se llama a CreateClient:

  • Se crea una instancia de HttpClient.
  • Se llama a la acción de configuración.

Para crear un cliente con nombre, pase su nombre a 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 [];
    }
}

En el código anterior, no es necesario especificar un nombre de host en la solicitud HTTP. El código puede pasar solo la ruta de acceso, ya que se usa la dirección base configurada del cliente.

Clientes con tipo

Clientes con tipo:

  • Proporcionan las mismas funciones que los clientes con nombre sin la necesidad de usar cadenas como claves.
  • Ofrece ayuda relativa al compilador e IntelliSense al consumir clientes.
  • Facilitan una sola ubicación para configurar un elemento HttpClient determinado e interactuar con él. Por ejemplo, es posible usar un solo cliente con tipo:
    • Para un único punto de conexión de back-end.
    • Para encapsular toda la lógica relacionada con el punto de conexión.
  • Funcionan con la inserción de dependencias y se pueden insertar en la aplicación cuando sea necesario.

Un cliente con tipo acepta un parámetro HttpClient en su constructor:

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

En el código anterior:

  • La configuración se establece cuando el cliente con tipo se agrega a la colección de servicios.
  • HttpClient se asigna como variable con ámbito de clase (campo) y se usa con las API expuestas.

Se pueden crear métodos específicos de la API que exponen la funcionalidad de HttpClient. Por ejemplo, el método GetUserTodosAsync encapsula el código para recuperar objetos Todo específicos del usuario.

En el código siguiente se llama a AddHttpClient para registrar una clase de cliente con 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");
    });

El cliente con tipo se registra como transitorio con inserción con dependencias, En el código anterior, AddHttpClient registra TodoService como servicio transitorio. Este registro usa un Factory Method para:

  1. Crea una instancia de HttpClient.
  2. Cree una instancia de TodoService, pasando la instancia de HttpClient a su constructor.

Importante

El uso de clientes con tipo en servicios singleton puede ser peligroso. Para más información, consulte la sección Evitar clientes con tipo en servicios singleton.

Nota

Al registrar un cliente con tipo con el método AddHttpClient<TClient>, el tipo TClient debe tener un constructor que acepte un parámetro HttpClient. Además, el tipo TClient no se debe registrar con el contenedor de DI por separado.

Clientes con nombre y con tipo

Los clientes con nombre y con tipo tienen sus propias fortalezas y puntos débiles. Hay una manera de combinar estos dos tipos de cliente para obtener lo mejor de ambos.

El caso de uso principal es el siguiente: use el mismo cliente con tipo, pero con dominios diferentes. Por ejemplo, tiene un servicio principal y otro secundario y ambos exponen exactamente la misma funcionalidad. Esto significa que puede usar el mismo cliente con tipo para ajustar el uso de HttpClient para emitir solicitudes, procesar respuestas y controlar errores. Se usará el mismo código exactamente, pero con configuraciones diferentes (dirección base, tiempo de espera y credenciales diferentes, por ejemplo).

En el siguiente ejemplo se usa el mismo cliente con tipo TodoService que se mostró en la sección clientes con tipo.

En primer lugar, registre los clientes con nombre y con tipo.

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

En el código anterior:

  • La primera llamada AddHttpClient registra un cliente con tipo TodoService con el nombre de primary. El HttpClient subyacente apunta al servicio principal y tiene un breve tiempo de espera.
  • La segunda llamada AddHttpClient registra un cliente con tipo TodoService con el nombre de secondary. El HttpClient subyacente apunta al servicio secundario y tiene un tiempo de espera más largo.
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);

En el código anterior:

  • Se recupera una instancia IHttpClientFactory del contenedor de inserción de dependencias para poder crear clientes con nombre a través de su método CreateClient.
  • Se recupera una instancia ITypedHttpClientFactory<TodoService> del contenedor de inserción de dependencias para poder crear clientes con tipo a través de su método CreateClient.
    • Esta sobrecarga CreateClient recibió un HttpClient con nombre (con la configuración adecuada) como parámetro.
    • El todoService creado está configurado para usar el servicio principal.

Nota:

El tipo IHttpClientFactory reside dentro de los espacios de nombres System.Net.Http, mientras que el tipo ITypedHttpClientFactory dentro del Microsoft.Extensions.Http.

Importante

Use la clase de implementación (en el ejemplo anterior, el TodoService) como parámetro de tipo para el ITypedHttpClientFactory. Incluso si también tiene una abstracción (como interfaz ITodoService), todavía tiene que usar la implementación. Si usa accidentalmente la abstracción (ITodoService), cuando llame a su CreateClient iniciará un 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);
}

En el código anterior:

  • Intenta emitir una solicitud en el servicio principal.
  • Si la solicitud agota el tiempo de espera (tarda más de 3 segundos), inicia un TaskCanceledException con un TimeoutException interno.
  • En caso de que se agote el tiempo de espera, se creará y utilizará un nuevo cliente que se dirigirá al servicio secundario.

Clientes generados

IHttpClientFactory se puede usar en combinación con bibliotecas de terceros, como Refit. Refit es una biblioteca de REST para .NET Permite definiciones de API REST declarativas, con lo que se asignan métodos de interfaz a puntos de conexión. Se genera una implementación de la interfaz dinámicamente por medio de RestService, usando HttpClient para realizar las llamadas HTTP externas.

Considere el siguiente tipo record:

namespace Shared;

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

El ejemplo siguiente se basa en el paquete NuGet Refit.HttpClientFactory y es una interfaz sencilla:

using Refit;
using Shared;

namespace GeneratedHttp.Example;

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

La interfaz de C# anterior:

  • Define un método denominado GetUserTodosAsync que devuelve una instancia de Task<Todo[]>.
  • Declara un atributo Refit.GetAttribute con la ruta de acceso y la cadena de consulta a la API externa.

Un cliente con tipo se puede agregar usando Refit para generar la implementación:

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

La interfaz definida se puede consumir cuando sea preciso, con la implementación proporcionada por la inserción de dependencias y Refit.

Realización de solicitudes POST, PUT y DELETE

En los ejemplos anteriores, todas las solicitudes HTTP usan el verbo GET de HTTP. HttpClient también admite otros verbos HTTP; por ejemplo:

  • POST
  • PUT
  • DELETE
  • PATCH

Para obtener una lista completa de los verbos HTTP admitidos, vea HttpMethod. Para obtener más información sobre cómo realizar solicitudes HTTP, consulte Envío de una solicitud mediante HttpClient.

En el ejemplo siguiente se muestra cómo hacer una solicitud 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();
}

En el código anterior, el método CreateItemAsync:

  • Serializa el parámetro Item en JSON mediante System.Text.Json. Usa una instancia de JsonSerializerOptions para configurar el proceso de serialización.
  • Crea una instancia de StringContent a fin de empaquetar el JSON serializado para enviarlo en el cuerpo de la solicitud de HTTP.
  • Llama a PostAsync para enviar el contenido de JSON a la URL especificada. Se trata de una URL relativa que se agrega a HttpClient.BaseAddress.
  • Llama a EnsureSuccessStatusCode para iniciar una excepción si el código de estado de la respuesta no indica que la operación se ha procesado correctamente.

HttpClient también admite otros tipos de contenido. Por ejemplo, MultipartContent y StreamContent. Para obtener una lista completa del contenido admitido, vea HttpContent.

En el ejemplo siguiente se muestra una solicitud 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();
}

El código anterior es muy similar al del ejemplo de POST. El método UpdateItemAsync llama a PutAsync en lugar de PostAsync.

En el ejemplo siguiente se muestra una solicitud HTTP DELETE:

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

    httpResponse.EnsureSuccessStatusCode();
}

En el código anterior, el método DeleteItemAsync llama a DeleteAsync. Debido a que las solicitudes DELETE HTTP normalmente no contienen ningún cuerpo, el método DeleteAsync no proporciona ninguna sobrecarga que acepte una instancia de HttpContent.

Para obtener más información sobre el uso de verbos HTTP diferentes con HttpClient, vea HttpClient.

Administración de la duración de HttpClient

Cada vez que se llama a CreateClient en IHttpClientFactory, se devuelve una nueva instancia de HttpClient. Se crea una instancia de HttpClientHandler por nombre de cliente. La fábrica administra la duración de las instancias de HttpClientHandler.

IHttpClientFactory almacena en caché las instancias de HttpClientHandler creadas por la fábrica para reducir el consumo de recursos. Cuando se crea una instancia de HttpClient, se puede reutilizar una instancia de HttpClientHandler de la memoria caché si su duración aún no ha expirado.

Se recomienda almacenar en caché los controladores, porque cada uno de ellos suele administrar su propio grupo de conexiones HTTP subyacente. Crear más controladores de los necesarios puede provocar el agotamiento del socket y retrasos en la conexión. Además, algunos controladores dejan las conexiones abiertas de forma indefinida, lo que puede ser un obstáculo a la hora de reaccionar ante los cambios de DNS.

La duración de controlador predeterminada es dos minutos. Para invalidar el valor predeterminado, llame a SetHandlerLifetime para cada cliente, en IServiceCollection:

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

Importante

Las instancias de HttpClient creadas por IHttpClientFactory están pensadas para ser de corta duración.

  • El reciclaje y la recreación de las instancias de HttpMessageHandler cuando expira su duración es esencial para que IHttpClientFactory se asegure de que los controladores reaccionan a los cambios de DNS. HttpClient se vincula a una instancia de controlador específica cuando se crea, por lo que deben solicitarse nuevas instancias de HttpClient a tiempo para asegurarse de que el cliente obtenga el controlador actualizado.

  • La eliminación de estas instancias de HttpClientcreadas por la fábrica no provocará el agotamiento de sockets, ya que su eliminación no desencadenará la eliminación de HttpMessageHandler. IHttpClientFactory hace un seguimiento y elimina los recursos que se usan para crear instancias de HttpClient, específicamente las instancias de HttpMessageHandler, en cuanto expira su duración y ya no hay ningún HttpClient usándolas.

Mantener activa una única instancia de HttpClient durante una larga duración es un patrón común que se puede usar como alternativa a IHttpClientFactory. Sin embargo, este patrón requiere configuración adicional, como PooledConnectionLifetime. Puede usar clientes de larga duración con PooledConnectionLifetime o clientes de corta duración creados con IHttpClientFactory. Para obtener información sobre qué estrategia usar en la aplicación, consulte Directrices para usar clientes HTTP.

Configuración de HttpMessageHandler

Puede que sea necesario controlar la configuración del elemento HttpMessageHandler interno usado por un cliente.

Se devuelve un IHttpClientBuilder cuando se agregan clientes con nombre o con tipo. Se puede usar el método de extensión ConfigurePrimaryHttpMessageHandler para definir un delegado en IServiceCollection. Este delegado servirá para crear y configurar el elemento principal HttpMessageHandler que ese cliente usa:

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

La configuración de HttClientHandler permite especificar un proxy para la instancia HttpClient entre otras diversas propiedades del controlador. Para obtener más información, consulte Proxy HTTP.

Configuración adicional

Hay varias opciones de configuración adicionales para controlar IHttpClientHandler:

Método Descripción
AddHttpMessageHandler Agrega un controlador de mensajes adicional para una clase HttpClient con nombre.
AddTypedClient Configura el enlace entre el tipo TClient y la clase HttpClient con nombre asociada a la interfaz IHttpClientBuilder.
ConfigureHttpClient Agrega un delegado que se usará para configurar un objeto HttpClient con nombre.
ConfigureHttpMessageHandlerBuilder Agrega un delegado que se usará para configurar controladores de mensajes mediante HttpMessageHandlerBuilder para un objeto HttpClient con nombre.
ConfigurePrimaryHttpMessageHandler Configura la clase HttpMessageHandler principal del contenedor de inserción de dependencias para una clase HttpClient con nombre.
RedactLoggedHeaders Establece la colección de nombres de encabezado HTTP para los que se deben censurar los valores antes del registro.
SetHandlerLifetime Establece el período de tiempo que se puede volver a usar una instancia de HttpMessageHandler. Cada cliente con nombre puede tener configurado su propio valor de duración de controlador.

Uso de IHttpClientFactory junto con SocketsHttpHandler

La implementación de HttpMessageHandler con SocketsHttpHandler se agregó en .NET Core 2.1, que permite configurar PooledConnectionLifetime. Esta configuración se usa para asegurarse de que el controlador reacciona a los cambios de DNS, por lo que el uso de SocketsHttpHandler se considera una alternativa al uso de IHttpClientFactory. Para obtener más información, consulte Directrices para usar HttpClient.

Sin embargo, se pueden usar SocketsHttpHandler y IHttpClientFactory conjuntamente para mejorar la capacidad de configuración. Al usar las dos API, obtiene la ventaja de poder definir una configuración de bajo nivel (por ejemplo, usando LocalCertificateSelectionCallback para la selección de certificados dinámicos) y de alto nivel (por ejemplo, aprovechando la integración de la inserción de dependencias y varias configuraciones de cliente).

Para usar ambas API:

  1. Especifique SocketsHttpHandler como PrimaryHandler y configure su PooledConnectionLifetime (por ejemplo, con un valor que antes estaba en HandlerLifetime).
  2. Dado que SocketsHttpHandler controlará el reciclaje y la agrupación de conexiones, ya no es necesario el reciclaje del controlador en el nivel de IHttpClientFactory. Si desea deshabilitarla, establezca HandlerLifetime en 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 con tipo en servicios singleton

Cuando se usa el enfoque de cliente con nombre, se inserta IHttpClientFactory en los servicios y se crean instancias de HttpClient llamando a CreateClient cada vez que se necesita un HttpClient.

Sin embargo, con el enfoque de cliente con tipo, los clientes con tipo son objetos transitorios que normalmente se insertan en los servicios. Esto puede causar un problema, porque un cliente con tipo se puede insertar en un servicio singleton.

Importante

Se espera que los clientes con tipo sean de corta duración en el mismo sentido que las instancias de HttpClient creadas por IHttpClientFactory (para obtener más información, consulte Administración de la duración de HttpClient). En cuanto se crea una instancia de cliente con tipo, IHttpClientFactory no tiene control sobre ella. Si una instancia de cliente con tipo se captura en un servicio singleton, puede impedir que reaccione a los cambios de DNS, lo que anularía uno de los propósitos de IHttpClientFactory.

Si tiene que usar instancias de HttpClient en un servicio singleton, tenga en cuenta las siguientes opciones:

  • Use el enfoque de cliente con nombre en su lugar, que inserta IHttpClientFactory en el servicio singleton y vuelve a crear instancias de HttpClient cuando es necesario.
  • Si necesita el enfoque de cliente con tipo, use SocketsHttpHandler con PooledConnectionLifetime configurado como controlador principal. Para más información sobre el uso de SocketsHttpHandler con IHttpClientFactory, consulte la sección Uso de IHttpClientFactory junto con SocketsHttpHandler.

Ámbitos del controlador de mensajes en IHttpClientFactory

IHttpClientFactory crea un ámbito de inserción de dependencias aparte por cada instancia de HttpMessageHandler. Estos ámbitos de inserción de dependencias son independientes de los de la aplicación (por ejemplo, un ámbito de solicitudes entrantes de ASP.NET o un ámbito de inserción de dependencias manual creado por el usuario), por lo que no compartirán instancias de servicio con ámbito. Los ámbitos de controlador de mensajes están vinculados a la duración del controlador y pueden sobrevivir a los ámbitos de la aplicación, lo que puede provocar, por ejemplo, la reutilización de la misma instancia de HttpMessageHandler con las mismas dependencias con ámbito insertadas entre varias solicitudes entrantes.

Diagrama que muestra dos ámbitos de inserción de dependencias de aplicación y un ámbito de controlador de mensajes aparte

Se recomienda encarecidamente a los usuarios no almacenar en caché información relacionada con el ámbito (por ejemplo, datos de HttpContext) dentro de las instancias de HttpMessageHandler y usar las dependencias con ámbito con precaución para evitar la filtración de información confidencial.

Si necesitara acceso a un ámbito de inserción de dependencias de aplicaciones desde el controlador de mensajes (por ejemplo, para la autenticación), encapsularía la lógica compatible con el ámbito en un objeto DelegatingHandler transitorio independiente y lo haría alrededor de una instancia de HttpMessageHandler de la caché de IHttpClientFactory. Para acceder a la llamada IHttpMessageHandlerFactory.CreateHandler del controlador en cualquier cliente con nombre registrado. En ese caso, crearía una instancia de HttpClient mediante el controlador construido.

Diagrama que muestra cómo obtener acceso a los ámbitos de inserción de dependencias de aplicaciones a través de un controlador de mensajes transitorio independiente y IHttpMessageHandlerFactory

En el ejemplo siguiente se muestra cómo crear un objeto HttpClient con un objeto DelegatingHandler compatible con el ámbito:

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

Una solución adicional puede ser usar un método de extensión para registrar un objeto DelegatingHandler compatible con el ámbito y anular el registro de IHttpClientFactory predeterminado mediante un servicio transitorio con acceso al ámbito actual de la aplicación:

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 obtener más información, vea el ejemplo completo.

Vea también