Uso de IHttpClientFactory para implementar solicitudes HTTP resistentes

Sugerencia

Este contenido es un extracto del libro electrónico, ".NET Microservices Architecture for Containerized .NET Applications" (Arquitectura de microservicios de .NET para aplicaciones de .NET contenedorizadas), disponible en Documentación de .NET o como un PDF descargable y gratuito que se puede leer sin conexión.

.NET Microservices Architecture for Containerized .NET Applications eBook cover thumbnail.

IHttpClientFactory es un contrato implementado por DefaultHttpClientFactory, una fábrica bien fundamentada disponible desde .NET Core 2.1 para crear instancias de HttpClient con el fin de usarlas en las aplicaciones.

Problemas con la clase HttpClient original disponible en .NET

La clase HttpClient original y bien conocida se puede usar fácilmente pero, en algunos casos, muchos desarrolladores no la usan de manera correcta.

Aunque esta clase implementa IDisposable, no se aconseja declarar y crear instancias de ella en una instrucción using porque, cuando el objeto HttpClient se desecha, el socket subyacente no se libera inmediatamente, lo que puede conducir a un problema de agotamiento del socket. Para obtener más información sobre este problema, vea la entrada de blog Está usando HttpClient mal y eso desestabiliza el software.

Por tanto, HttpClient está diseñado para que se cree una instancia una vez y se reutilice durante la vida de una aplicación. Crear una instancia de una clase HttpClient para cada solicitud agotará el número de sockets disponibles bajo cargas pesadas. Ese problema generará errores SocketException. Los enfoques posibles para solucionar ese problema se basan en la creación del objeto HttpClient como singleton o estático, como se explica en este artículo de Microsoft sobre el uso de HttpClient. Puede tratarse de una buena solución para las aplicaciones de consola de corta duración o elementos similares que se ejecutan varias veces al día.

Otra incidencia a la que los desarrolladores deben hacer frente es cuando se usa una instancia compartida de HttpClient en procesos de larga duración. En una situación en la que se crean instancias del HttpClient como un singleton o un objeto estático, los cambios de DNS no se pueden controlar, tal y como se describe en esta incidencia del repositorio de GitHub sobre dotnet/runtime.

Realmente el problema no está en HttpClient, sino en el constructor predeterminado de HttpClient, ya que crea una instancia concreta de HttpMessageHandler, que es la que plantea los problemas de agotamiento de sockets y los cambios de DNS mencionados anteriormente.

Para solucionar los problemas mencionados anteriormente y para que las instancias de HttpClient se puedan administrar, .NET Core 2.1 ha introducido dos enfoques, uno de los cuales es IHttpClientFactory. Se trata de una interfaz que se usa para configurar y crear instancias de HttpClient en una aplicación mediante Inserción de dependencias (DI). También proporciona extensiones para el middleware basado en Polly a fin de aprovechar los controladores de delegación en HttpClient.

La alternativa es usar SocketsHttpHandler con PooledConnectionLifetime configurado. Este enfoque se aplica a instancias de larga duración ,static o HttpClient de singleton. Para obtener más información sobre las distintas estrategias, vea Directrices de HttpClient para .NET.

Polly es una biblioteca de control de errores transitorios que ayuda a los desarrolladores a agregar resistencia a sus aplicaciones mediante el uso de directivas predefinidas de manera fluida y segura para subprocesos.

Ventajas de usar IHttpClientFactory

La implementación actual de IHttpClientFactory, que también implementa IHttpMessageHandlerFactory, reporta las siguientes ventajas:

  • Proporciona una ubicación central para denominar y configurar instancias lógicas de HttpClient. Por ejemplo, puede configurar un cliente (Agente de servicio) preconfigurado para acceder a un microservicio concreto.
  • Codifica el concepto de software intermedio de salida a través de controladores de delegación en HttpClient e implemente software intermedio basado en Polly para aprovechar las directivas de resistencia de Polly.
  • HttpClient ya posee el concepto de controladores de delegación, que se pueden vincular entre sí para las solicitudes HTTP salientes. Los clientes HTTP se pueden registrar en la fábrica y se puede usar un controlador de Polly que permite utilizar directivas de Polly para el reintento, interruptores, etc.
  • Administre la duración de HttpMessageHandler para evitar los problemas mencionados y los que se puedan producir al administrar las duraciones de HttpClient usted mismo.

Sugerencia

Las instancias de HttpClient insertadas mediante DI se pueden eliminar de forma segura, porque el elemento HttpMessageHandler asociado lo administra la fábrica. Las instancias de HttpClient insertadas son transitorias desde punto de vista de DI, mientras que las de HttpMessageHandler se pueden considerar como con ámbito. Las instancias de HttpMessageHandler tienen sus propios ámbitos de DI, independientes de los ámbitos de la aplicación (por ejemplo, ámbitos de solicitud de entrada de ASP.NET). Para obtener más información, vea Uso de HttpClientFactory en .NET.

Nota

La implementación de IHttpClientFactory (DefaultHttpClientFactory) está estrechamente ligada a la implementación de la inserción de dependencias (DI) en el paquete de NuGet Microsoft.Extensions.DependencyInjection. Si necesita usar HttpClient sin DI o con otras implementaciones de DI, considere la posibilidad de usar un elemento static o una singleton HttpClient con la configuración PooledConnectionLifetime. A fin de obtener más información, vea Directrices de HttpClient para .NET.

Varias formas de usar IHttpClientFactory

Hay varias formas de usar IHttpClientFactory en la aplicación:

  • Uso básico
  • Usar clientes con nombre.
  • Usar clientes con tipo.
  • Usar clientes generados.

En pro de la brevedad, esta guía muestra la manera más estructurada para usar IHttpClientFactory, que consiste en usar clientes con tipo (el patrón de agente de servicio). Pero todas las opciones están documentadas e incluidas actualmente en este artículo que trata sobre el uso de HttpClientFactoryIHttpClientFactory.

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, vea Directrices para usar clientes HTTP.

Cómo usar clientes con tipo con IHttpClientFactory

Así pues, ¿qué es un "Cliente con tipo"? Es solo un elemento HttpClient que está preconfigurado para un uso específico. Esta configuración puede incluir valores específicos como un servidor base, encabezados HTTP o tiempos de espera.

En el diagrama siguiente se muestra cómo se usan los clientes con tipo con IHttpClientFactory:

Diagram showing how typed clients are used with IHttpClientFactory.

Figura 8-4. Uso de IHttpClientFactory con clases de cliente con tipo.

En la imagen anterior, un servicio ClientService (usado por un controlador o código de cliente) utiliza un cliente HttpClient creado por la fábrica IHttpClientFactory registrada. Este generador asigna a HttpClient un elemento HttpMessageHandler desde un grupo. El cliente HttpClient se puede configurar con las directivas de Polly al registrar la fábrica IHttpClientFactory en el contenedor de DI con el método de extensión AddHttpClient.

Para configurar la estructura anterior, agregue IHttpClientFactory a la aplicación mediante la instalación del paquete de NuGet Microsoft.Extensions.Http, que incluye el método de extensión AddHttpClient para IServiceCollection. Este método de extensión registra la clase interna DefaultHttpClientFactory que se va a usar como singleton en la interfaz IHttpClientFactory. Define una configuración transitoria para HttpMessageHandlerBuilder. Este controlador de mensajes (el objeto HttpMessageHandler), tomado de un grupo, lo usa el HttpClient devuelto desde la fábrica.

En el fragmento de código siguiente, puede ver cómo se puede utilizar AddHttpClient() para registrar clientes con tipo (agentes de servicio) que necesitan usar HttpClient.

// Program.cs
//Add http client services at ConfigureServices(IServiceCollection services)
builder.Services.AddHttpClient<ICatalogService, CatalogService>();
builder.Services.AddHttpClient<IBasketService, BasketService>();
builder.Services.AddHttpClient<IOrderingService, OrderingService>();

Al registrar los servicios de cliente tal como se muestra en el fragmento de código anterior, DefaultClientFactory crea un elemento HttpClient estándar para cada servicio. El cliente con tipo se registra como transitorio con contenedor DI (de inserción con dependencia). En el código anterior, AddHttpClient() registra CatalogService, BasketService, OrderingService como servicios transitorios para que se puedan insertar y consumir directamente sin necesidad de registros adicionales.

También puede agregar una configuración específica de instancia en el registro para, por ejemplo, configurar la dirección base y agregar algunas directivas de resistencia, tal como se muestra a continuación:

builder.Services.AddHttpClient<ICatalogService, CatalogService>(client =>
{
    client.BaseAddress = new Uri(builder.Configuration["BaseUrl"]);
})
    .AddPolicyHandler(GetRetryPolicy())
    .AddPolicyHandler(GetCircuitBreakerPolicy());

En este ejemplo siguiente, puede ver la configuración de una de las directivas anteriores:

static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
    return HttpPolicyExtensions
        .HandleTransientHttpError()
        .OrResult(msg => msg.StatusCode == System.Net.HttpStatusCode.NotFound)
        .WaitAndRetryAsync(6, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}

Puede encontrar más detalles sobre el uso de Polly en el artículo siguiente.

Duraciones de HttpClient

Cada vez que se obtiene un objeto HttpClient de IHttpClientFactory, se devuelve una nueva instancia. Pero cada cliente HttpClient usa un controlador HttpMessageHandler que IHttpClientFactory agrupa y vuelve a usar para reducir el consumo de recursos, siempre y cuando la vigencia de HttpMessageHandlerno haya expirado.

La agrupación de controladores es conveniente porque cada controlador suele administrar sus propias conexiones HTTP subyacentes. Crear más controladores de lo necesario puede provocar 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.

Los objetos HttpMessageHandler del grupo tienen una duración que es el período de tiempo que se puede reutilizar una instancia de HttpMessageHandler en el grupo. El valor predeterminado es de dos minutos, pero se puede invalidar por cada cliente con tipo. Para ello, llame a SetHandlerLifetime() en el IHttpClientBuilder que se devuelve cuando se crea el cliente, como se muestra en el siguiente código:

//Set 5 min as the lifetime for the HttpMessageHandler objects in the pool used for the Catalog Typed Client
builder.Services.AddHttpClient<ICatalogService, CatalogService>()
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));

Cada cliente con tipo puede tener configurado su propio valor de duración de controlador. Establezca la duración en InfiniteTimeSpan para deshabilitar la expiración del controlador.

Implementar las clases de cliente con tipo que usan el HttpClient insertado y configurado

Como paso anterior, debe tener las clases de cliente con tipo definidas, como las del código de ejemplo (por ejemplo, "BasketService", "CatalogService", "OrderingService", etc.). Un cliente con tipo es una clase que acepta un objeto HttpClient (insertado a través de su constructor) y lo usa para llamar a algún servicio remoto de HTTP. Por ejemplo:

public class CatalogService : ICatalogService
{
    private readonly HttpClient _httpClient;
    private readonly string _remoteServiceBaseUrl;

    public CatalogService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<Catalog> GetCatalogItems(int page, int take,
                                               int? brand, int? type)
    {
        var uri = API.Catalog.GetAllCatalogItems(_remoteServiceBaseUrl,
                                                 page, take, brand, type);

        var responseString = await _httpClient.GetStringAsync(uri);

        var catalog = JsonConvert.DeserializeObject<Catalog>(responseString);
        return catalog;
    }
}

El cliente con tipo (CatalogService en el ejemplo) se activa mediante DI (inserción de dependencias), lo que significa que puede aceptar cualquier servicio registrado en su constructor, además de HttpClient.

Un cliente con tipo es realmente un objeto transitorio, lo que significa que, cada vez que se necesita uno, se crea una instancia. Recibe una nueva instancia de HttpClient cada vez que se construye. Pero los objetos HttpMessageHandler del grupo son los objetos que varias instancias de HttpClient reutilizan.

Usar las clases de cliente con tipo

Por último, una vez que haya implementado las clases con tipo, puede registrarlas y configurarlas con AddHttpClient(). Después de eso, puede usarlos dondequiera que los servicios se inserten mediante DI, como en el código de la página de Razor o en un controlador de aplicación web MVC, que se muestra en el código siguiente de eShopOnContainers:

namespace Microsoft.eShopOnContainers.WebMVC.Controllers
{
    public class CatalogController : Controller
    {
        private ICatalogService _catalogSvc;

        public CatalogController(ICatalogService catalogSvc) =>
                                                           _catalogSvc = catalogSvc;

        public async Task<IActionResult> Index(int? BrandFilterApplied,
                                               int? TypesFilterApplied,
                                               int? page,
                                               [FromQuery]string errorMsg)
        {
            var itemsPage = 10;
            var catalog = await _catalogSvc.GetCatalogItems(page ?? 0,
                                                            itemsPage,
                                                            BrandFilterApplied,
                                                            TypesFilterApplied);
            //… Additional code
        }

        }
}

Hasta ahora, el fragmento de código anterior tan solo muestra el ejemplo de realizar solicitudes HTTP normales. Pero la "magia" viene en las secciones siguientes, donde se muestra cómo todas las solicitudes HTTP que realiza HttpClient pueden tener directivas resistentes como, por ejemplo, reintentos con retroceso exponencial, disyuntores, características de seguridad que usan tokens de autenticación o incluso cualquier otra característica personalizada. Y todo esto se puede hacer simplemente agregando directivas y delegando controladores a los clientes con tipo registrados.

Recursos adicionales