Compartir a través de


Realización de solicitudes HTTP mediante IHttpClientFactory en ASP.NET Core

Nota:

Esta no es la versión más reciente de este artículo. Para la versión actual, consulte la versión de .NET 9 de este artículo.

Advertencia

Esta versión de ASP.NET Core ya no se admite. Para obtener más información, consulta la Directiva de soporte técnico de .NET y .NET Core. Para la versión actual, consulta la versión .NET 8 de este artículo.

Importante

Esta información hace referencia a un producto en versión preliminar, el cual puede sufrir importantes modificaciones antes de que se publique la versión comercial. Microsoft no proporciona ninguna garantía, expresa o implícita, con respecto a la información proporcionada aquí.

Para la versión actual, consulte la versión de .NET 9 de este artículo.

Por Kirk Larkin, Steve Gordon, Glenn Condron y Ryan Nowak.

Se puede registrar y usar una interfaz IHttpClientFactory para crear y configurar instancias de HttpClient en una aplicación. IHttpClientFactory ofrece las ventajas siguientes:

  • Proporciona una ubicación central para denominar y configurar instancias de HttpClient lógicas. Por ejemplo, se podría registrar un cliente llamado github y configurarlo para acceder a GitHub. Se puede registrar un cliente predeterminado para el acceso general.
  • Codifica el concepto de middleware de salida a través de la delegación de controladores en HttpClient. Proporciona extensiones para el middleware basado en Polly a fin de aprovechar los controladores de delegación en HttpClient.
  • Administra la agrupación y la duración de las instancias de HttpClientMessageHandler subyacentes. La administración automática evita los problemas comunes de DNS (Sistema de nombres de dominio) 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.

En el código de ejemplo de la versión este tema se usa System.Text.Json para deserializar el contenido JSON devuelto en las respuestas HTTP. Para obtener ejemplos en los que se usan Json.NET y ReadAsAsync<T>, utilice el selector de versión para seleccionar una versión 2.x de este tema.

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

Registre IHttpClientFactory mediante una llamada a AddHttpClient en Program.cs:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddHttpClient();

Se puede solicitar una instancia de IHttpClientFactory mediante la inserción de dependencias (DI). En el código siguiente se usa IHttpClientFactory para crear una instancia de HttpClient:

public class BasicModel : PageModel
{
    private readonly IHttpClientFactory _httpClientFactory;

    public BasicModel(IHttpClientFactory httpClientFactory) =>
        _httpClientFactory = httpClientFactory;

    public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }

    public async Task OnGet()
    {
        var httpRequestMessage = new HttpRequestMessage(
            HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches")
        {
            Headers =
            {
                { HeaderNames.Accept, "application/vnd.github.v3+json" },
                { HeaderNames.UserAgent, "HttpRequestsSample" }
            }
        };

        var httpClient = _httpClientFactory.CreateClient();
        var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage);

        if (httpResponseMessage.IsSuccessStatusCode)
        {
            using var contentStream =
                await httpResponseMessage.Content.ReadAsStreamAsync();
            
            GitHubBranches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(contentStream);
        }
    }
}

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 HttpClient de tienen otra configuración.

Especifique la configuración de un objeto HttpClient con nombre durante su registro en Program.cs:

builder.Services.AddHttpClient("GitHub", httpClient =>
{
    httpClient.BaseAddress = new Uri("https://api.github.com/");

    // using Microsoft.Net.Http.Headers;
    // The GitHub API requires two headers.
    httpClient.DefaultRequestHeaders.Add(
        HeaderNames.Accept, "application/vnd.github.v3+json");
    httpClient.DefaultRequestHeaders.Add(
        HeaderNames.UserAgent, "HttpRequestsSample");
});

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

  • La dirección base. https://api.github.com/.
  • Dos encabezados necesarios para trabajar con la API de GitHub.

CreateClient

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:

public class NamedClientModel : PageModel
{
    private readonly IHttpClientFactory _httpClientFactory;

    public NamedClientModel(IHttpClientFactory httpClientFactory) =>
        _httpClientFactory = httpClientFactory;

    public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }

    public async Task OnGet()
    {
        var httpClient = _httpClientFactory.CreateClient("GitHub");
        var httpResponseMessage = await httpClient.GetAsync(
            "repos/dotnet/AspNetCore.Docs/branches");

        if (httpResponseMessage.IsSuccessStatusCode)
        {
            using var contentStream =
                await httpResponseMessage.Content.ReadAsStreamAsync();
            
            GitHubBranches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(contentStream);
        }
    }
}

En el código anterior, no es necesario especificar un nombre de host en la solicitud. 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.
  • Ofrecen 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:

public class GitHubService
{
    private readonly HttpClient _httpClient;

    public GitHubService(HttpClient httpClient)
    {
        _httpClient = httpClient;

        _httpClient.BaseAddress = new Uri("https://api.github.com/");

        // using Microsoft.Net.Http.Headers;
        // The GitHub API requires two headers.
        _httpClient.DefaultRequestHeaders.Add(
            HeaderNames.Accept, "application/vnd.github.v3+json");
        _httpClient.DefaultRequestHeaders.Add(
            HeaderNames.UserAgent, "HttpRequestsSample");
    }

    public async Task<IEnumerable<GitHubBranch>?> GetAspNetCoreDocsBranchesAsync() =>
        await _httpClient.GetFromJsonAsync<IEnumerable<GitHubBranch>>(
            "repos/dotnet/AspNetCore.Docs/branches");
}

En el código anterior:

  • La configuración se mueve al cliente con tipo.
  • La instancia de HttpClient proporcionada se almacena como un campo privado.

Se pueden crear métodos específicos de la API que exponen la funcionalidad de HttpClient. Por ejemplo, el método GetAspNetCoreDocsBranches encapsula el código para recuperar las ramas de GitHub de documentos.

En el código siguiente se llama a AddHttpClient en Program.cs para registrar una clase de cliente GitHubService con tipo:

builder.Services.AddHttpClient<GitHubService>();

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

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

y se puede insertar y consumir directamente:

public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public TypedClientModel(GitHubService gitHubService) =>
        _gitHubService = gitHubService;

    public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }

    public async Task OnGet()
    {
        try
        {
            GitHubBranches = await _gitHubService.GetAspNetCoreDocsBranchesAsync();
        }
        catch (HttpRequestException)
        {
            // ...
        }
    }
}

La configuración de un cliente con tipo se puede especificar también durante su registro en Program.cs, en lugar de en su constructor:

builder.Services.AddHttpClient<GitHubService>(httpClient =>
{
    httpClient.BaseAddress = new Uri("https://api.github.com/");

    // ...
});

Clientes generados

IHttpClientFactory se puede usar en combinación con bibliotecas de terceros, como Refit. Refit es una biblioteca de REST para .NET. Convierte las API de REST en interfaces en vivo. Llame a AddRefitClient para generar una implementación dinámica de una interfaz, que usa HttpClient para realizar las llamadas HTTP externas.

Una interfaz personalizada representa la API externa:

public interface IGitHubClient
{
    [Get("/repos/dotnet/AspNetCore.Docs/branches")]
    Task<IEnumerable<GitHubBranch>> GetAspNetCoreDocsBranchesAsync();
}

Llame a AddRefitClient para generar la implementación dinámica y, luego, llame a ConfigureHttpClient para configurar el elemento HttpClient subyacente:

builder.Services.AddRefitClient<IGitHubClient>()
    .ConfigureHttpClient(httpClient =>
    {
        httpClient.BaseAddress = new Uri("https://api.github.com/");

        // using Microsoft.Net.Http.Headers;
        // The GitHub API requires two headers.
        httpClient.DefaultRequestHeaders.Add(
            HeaderNames.Accept, "application/vnd.github.v3+json");
        httpClient.DefaultRequestHeaders.Add(
            HeaderNames.UserAgent, "HttpRequestsSample");
    });

Use DI para acceder a la implementación dinámica de IGitHubClient:

public class RefitModel : PageModel
{
    private readonly IGitHubClient _gitHubClient;

    public RefitModel(IGitHubClient gitHubClient) =>
        _gitHubClient = gitHubClient;

    public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }

    public async Task OnGet()
    {
        try
        {
            GitHubBranches = await _gitHubClient.GetAspNetCoreDocsBranchesAsync();
        }
        catch (ApiException)
        {
            // ...
        }
    }
}

Realización de solicitudes POST, PUT y DELETE

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

  • POST
  • PUT
  • SUPRIMIR
  • PATCH

Para obtener una lista completa de los verbos HTTP admitidos, vea HttpMethod.

En el ejemplo siguiente se muestra cómo hacer una solicitud POST HTTP:

public async Task CreateItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem),
        Encoding.UTF8,
        Application.Json); // using static System.Net.Mime.MediaTypeNames;

    using var httpResponseMessage =
        await _httpClient.PostAsync("/api/TodoItems", todoItemJson);

    httpResponseMessage.EnsureSuccessStatusCode();
}

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

  • Serializa el parámetro TodoItem en JSON mediante System.Text.Json.
  • 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 realizado correctamente.

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

En el ejemplo siguiente se muestra una solicitud PUT HTTP:

public async Task SaveItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem),
        Encoding.UTF8,
        Application.Json);

    using var httpResponseMessage =
        await _httpClient.PutAsync($"/api/TodoItems/{todoItem.Id}", todoItemJson);

    httpResponseMessage.EnsureSuccessStatusCode();
}

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

En el ejemplo siguiente se muestra una solicitud DELETE HTTP:

public async Task DeleteItemAsync(long itemId)
{
    using var httpResponseMessage =
        await _httpClient.DeleteAsync($"/api/TodoItems/{itemId}");

    httpResponseMessage.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, consulta HttpClient.

Middleware de solicitud saliente

HttpClient tiene el concepto de controladores de delegación, que se pueden vincular entre sí para las solicitudes HTTP salientes. IHttpClientFactory:

  • simplifica la definición de controladores que se aplicarán por cada cliente con nombre.
  • Admite el registro y encadenamiento de varios controladores para crear una canalización de middleware de solicitud saliente. Cada uno de estos controladores es capaz de realizar la tarea antes y después de la solicitud de salida. Este patrón:
    • Es similar a la canalización de middleware de entrada de ASP.NET Core.
    • Proporciona un mecanismo para administrar los intereses transversales relacionados con las solicitudes HTTP, como:
      • el almacenamiento en caché
      • el control de errores
      • la serialización
      • el registro

Para crear un controlador de delegación:

  • Derívalo de DelegatingHandler.
  • Reemplaza SendAsync. Ejecuta el código antes de pasar la solicitud al siguiente controlador de la canalización:
public class ValidateHeaderHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains("X-API-KEY"))
        {
            return new HttpResponseMessage(HttpStatusCode.BadRequest)
            {
                Content = new StringContent(
                    "The API key header X-API-KEY is required.")
            };
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

El código anterior comprueba si el encabezado X-API-KEY está en la solicitud. Si falta X-API-KEY, se devuelve BadRequest.

Se puede agregar más de un controlador a la configuración de una instancia de HttpClient con Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessageHandler:

builder.Services.AddTransient<ValidateHeaderHandler>();

builder.Services.AddHttpClient("HttpMessageHandler")
    .AddHttpMessageHandler<ValidateHeaderHandler>();

En el código anterior, ValidateHeaderHandler se ha registrado con inserción de dependencias. Una vez registrado, se puede llamar a AddHttpMessageHandler, pasando el tipo del controlador.

Se pueden registrar varios controladores en el orden en que deben ejecutarse. Cada controlador contiene el siguiente controlador hasta que el último HttpClientHandler ejecuta la solicitud:

builder.Services.AddTransient<SampleHandler1>();
builder.Services.AddTransient<SampleHandler2>();

builder.Services.AddHttpClient("MultipleHttpMessageHandlers")
    .AddHttpMessageHandler<SampleHandler1>()
    .AddHttpMessageHandler<SampleHandler2>();

En el código anterior, SampleHandler1 se ejecuta primero, antes que SampleHandler2.

Uso de la inserción de dependencias en el middleware de solicitud de salida

Cuando IHttpClientFactory crea un nuevo controlador de delegación, usa la inserción de dependencias para cumplir con los parámetros de constructor del controlador. IHttpClientFactory crea un ámbito de inserción de dependencias independiente para cada controlador, lo que puede provocar un comportamiento sorprendente cuando un controlador consume un servicio con ámbito.

Por ejemplo, considera la siguiente interfaz y su implementación, que representa una tarea como una operación con un identificador, OperationId:

public interface IOperationScoped
{
    string OperationId { get; }
}

public class OperationScoped : IOperationScoped
{
    public string OperationId { get; } = Guid.NewGuid().ToString()[^4..];
}

Como sugiere su nombre, IOperationScoped se registra con la inserción de dependencias mediante una duración con ámbito:

builder.Services.AddScoped<IOperationScoped, OperationScoped>();

El siguiente controlador de delegación consume y usa IOperationScoped para establecer el encabezado X-OPERATION-ID para la solicitud de salida:

public class OperationHandler : DelegatingHandler
{
    private readonly IOperationScoped _operationScoped;

    public OperationHandler(IOperationScoped operationScoped) =>
        _operationScoped = operationScoped;

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Headers.Add("X-OPERATION-ID", _operationScoped.OperationId);

        return await base.SendAsync(request, cancellationToken);
    }
}

En la descarga de HttpRequestsSample, ve a /Operation y actualiza la página. El valor del ámbito de la solicitud cambia para cada solicitud, pero el valor del ámbito del controlador solo cambia cada 5 segundos.

Los controladores pueden depender de servicios de cualquier ámbito. Los servicios de los que dependen los controladores se eliminan cuando se elimina el controlador.

Usa uno de los siguientes enfoques para compartir el estado por solicitud con controladores de mensajes:

Usar controladores basados en Polly

IHttpClientFactory se integra con la biblioteca de terceros Polly. Polly es una biblioteca con capacidades de resistencia y control de errores transitorios para .NET. Permite a los desarrolladores expresar directivas como, por ejemplo, de reintento, interruptor, tiempo de espera, aislamiento compartimentado y reserva de forma fluida y segura para los subprocesos.

Se proporcionan métodos de extensión para hacer posible el uso de directivas de Polly con instancias de HttpClient configuradas. Las extensiones de Polly permiten agregar controladores basados en Polly a los clientes. Polly requiere el paquete NuGet Microsoft.Extensions.Http.Polly.

Control de errores transitorios

Los errores se suelen producir cuando las llamadas HTTP externas son transitorias. AddTransientHttpErrorPolicy permite definir una directiva para controlar los errores transitorios. Las directivas configuradas con AddTransientHttpErrorPolicy controlan las respuestas siguientes:

AddTransientHttpErrorPolicy proporciona acceso a un objeto PolicyBuilder configurado para controlar los errores que representan un posible error transitorio:

builder.Services.AddHttpClient("PollyWaitAndRetry")
    .AddTransientHttpErrorPolicy(policyBuilder =>
        policyBuilder.WaitAndRetryAsync(
            3, retryNumber => TimeSpan.FromMilliseconds(600)));

En el código anterior, se define una directiva WaitAndRetryAsync. Las solicitudes erróneas se reintentan hasta tres veces con un retardo de 600 ms entre intentos.

Seleccionar directivas dinámicamente

Los métodos de extensión se proporcionan para agregar controladores basados en Polly, por ejemplo, AddPolicyHandler. La siguiente sobrecarga de AddPolicyHandler inspecciona la solicitud para decidir qué directiva se debe aplicar:

var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

builder.Services.AddHttpClient("PollyDynamic")
    .AddPolicyHandler(httpRequestMessage =>
        httpRequestMessage.Method == HttpMethod.Get ? timeoutPolicy : longTimeoutPolicy);

En el código anterior, si la solicitud GET saliente es del tipo HTTP, se aplica un tiempo de espera de 10 segundos. En cualquier otro método HTTP, se usa un tiempo de espera de 30 segundos.

Agregar varios controladores de Polly

Es común anidar las directivas de Polly:

builder.Services.AddHttpClient("PollyMultiple")
    .AddTransientHttpErrorPolicy(policyBuilder =>
        policyBuilder.RetryAsync(3))
    .AddTransientHttpErrorPolicy(policyBuilder =>
        policyBuilder.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));

En el ejemplo anterior:

  • Se agregan dos controladores.
  • El primer controlador usa AddTransientHttpErrorPolicy para agregar una directiva de reintentos. Las solicitudes con error se reintentan hasta tres veces.
  • La segunda llamada a AddTransientHttpErrorPolicy agrega una directiva de interruptor. Las solicitudes externas adicionales se bloquean durante 30 segundos si se producen cinco intentos con error seguidos. Las directivas de interruptor tienen estado. Así, todas las llamadas realizadas a través de este cliente comparten el mismo estado de circuito.

Agregar directivas desde el Registro de Polly

Una forma de administrar las directivas usadas habitualmente consiste en definirlas una vez y registrarlas con PolicyRegistry. Por ejemplo:

var timeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeoutPolicy = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

var policyRegistry = builder.Services.AddPolicyRegistry();

policyRegistry.Add("Regular", timeoutPolicy);
policyRegistry.Add("Long", longTimeoutPolicy);

builder.Services.AddHttpClient("PollyRegistryRegular")
    .AddPolicyHandlerFromRegistry("Regular");

builder.Services.AddHttpClient("PollyRegistryLong")
    .AddPolicyHandlerFromRegistry("Long");

En el código anterior:

  • Se agregan dos directivas, Regular y Long, al registro de Polly.
  • AddPolicyHandlerFromRegistry configura clientes con nombre individuales para que usen estas directivas del registro de Polly.

Para más información sobre IHttpClientFactory y las integraciones de Polly, vea la wiki de Polly.

HttpClient y administración de la duración

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

IHttpClientFactory agrupa las instancias de HttpMessageHandler creadas por Factory para reducir el consumo de recursos. Se puede reutilizar una instancia de HttpMessageHandler del grupo al crear una instancia de HttpClient si su duración aún no ha expirado.

Se recomienda agrupar controladores porque cada uno de ellos normalmente administra sus propias conexiones HTTP subyacentes. Crear más controladores de los necesarios puede provocar retrasos en la conexión. Además, algunos controladores dejan las conexiones abiertas de forma indefinida, lo que puede impedir que el controlador reaccione ante los cambios de DNS (Sistema de nombres de dominio).

La duración de controlador predeterminada es dos minutos. El valor predeterminado se puede reemplazar de forma individual en cada cliente con nombre:

builder.Services.AddHttpClient("HandlerLifetime")
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));

Normalmente, las instancias de HttpClient se pueden trata como objetos de .NET que no requieren eliminación. ya que se cancelan las solicitudes salientes y la instancia de HttpClient determinada no se puede usar después de llamar a Dispose. IHttpClientFactory realiza un seguimiento y elimina los recursos que usan las instancias de HttpClient.

Mantener una sola instancia de HttpClient activa durante un período prolongado es un patrón común que se utiliza antes de la concepción de IHttpClientFactory. Este patrón se convierte en innecesario tras la migración a IHttpClientFactory.

Alternativas a IHttpClientFactory

El uso de IHttpClientFactory en una aplicación habilitada para la inserción de dependencias evita lo siguiente:

  • Problemas de agotamiento de recursos mediante la agrupación de instancias de HttpMessageHandler.
  • Problemas de DNS obsoletos al recorrer las instancias de HttpMessageHandler a intervalos regulares.

Existen formas alternativas de solucionar los problemas anteriores mediante una instancia de SocketsHttpHandler de larga duración.

  • Cree una instancia de SocketsHttpHandler al iniciar la aplicación y úsela para la vida útil de la aplicación.
  • Configure PooledConnectionLifetime con un valor adecuado en función de los tiempos de actualización de DNS.
  • Cree instancias de HttpClient mediante new HttpClient(handler, disposeHandler: false) según sea necesario.

Los enfoques anteriores solucionan los problemas de administración de recursos que IHttpClientFactory resuelve de forma similar.

  • SocketsHttpHandler comparte las conexiones entre las instancias de HttpClient. Este uso compartido impide el agotamiento del socket.
  • SocketsHttpHandler recorre las conexiones según PooledConnectionLifetime para evitar problemas de DNS obsoletos.

Registro

Los clientes que se han creado a través de IHttpClientFactory registran mensajes de registro de todas las solicitudes. Habilite el nivel de información adecuado en la configuración del registro para ver los mensajes de registro predeterminados. El registro de más información, como el registro de encabezados de solicitud, solo se incluye en el nivel de seguimiento.

La categoría de registro usada en cada cliente incluye el nombre del cliente. Un cliente denominado MyNamedClient, por ejemplo, registra mensajes con una categoría de "System.Net.Http.HttpClient.MyNamedClient.LogicalHandler". Los mensajes con el sufijo LogicalHandler se producen fuera de la canalización de controlador de la solicitud. En la solicitud, los mensajes se registran antes de que cualquier otro controlador de la canalización haya procesado la solicitud. En la respuesta, los mensajes se registran después de que cualquier otro controlador de la canalización haya recibido la respuesta.

El registro también se produce dentro de la canalización de controlador de la solicitud. En el ejemplo MyNamedClient, esos mensajes se registran con la categoría de registro "System.Net.Http.HttpClient.MyNamedClient.ClientHandler". En la solicitud, esto tiene lugar después de que todos los demás controladores se hayan ejecutado y justo antes de que se envíe la solicitud. En la respuesta, este registro incluye el estado de la respuesta antes de que vuelva a pasar por la canalización del controlador.

Al habilitar el registro tanto dentro como fuera de la canalización, se podrán inspeccionar los cambios realizados por otros controladores de la canalización. Esto puede incluir cambios en los encabezados de solicitud o en el código de estado de la respuesta.

La inclusión del nombre del cliente en la categoría de registro permite filtrar el registro para clientes con nombre específicos.

Configurar 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. Este delegado servirá para crear y configurar el elemento principal HttpMessageHandler que ese cliente usa:

builder.Services.AddHttpClient("ConfiguredHttpMessageHandler")
    .ConfigurePrimaryHttpMessageHandler(() =>
        new HttpClientHandler
        {
            AllowAutoRedirect = true,
            UseDefaultCredentials = true
        });

Cookies

Las instancias de HttpMessageHandler agrupadas generan objetos CookieContainer que se comparten. El uso compartido de objetos CookieContainer no previsto suele generar código incorrecto. En el caso de las aplicaciones que requieren cookies, tenga en cuenta lo siguiente:

  • Deshabilitación del control automático de cookies
  • Evitar IHttpClientFactory

Llame a ConfigurePrimaryHttpMessageHandler para deshabilitar el control automático de cookies:

builder.Services.AddHttpClient("NoAutomaticCookies")
    .ConfigurePrimaryHttpMessageHandler(() =>
        new HttpClientHandler
        {
            UseCookies = false
        });

Uso de IHttpClientFactory en una aplicación de consola

En una aplicación de consola, agregue las siguientes referencias de paquete al proyecto:

En el ejemplo siguiente:

  • IHttpClientFactory y GitHubService se registran en el contenedor de servicio del host genérico.
  • GitHubService se solicita desde DI, que, a su vez, solicita una instancia de IHttpClientFactory.
  • GitHubService usa IHttpClientFactory para crear una instancia de HttpClient, que usa para recuperar ramas de GitHub de documentos.
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var host = new HostBuilder()
    .ConfigureServices(services =>
    {
        services.AddHttpClient();
        services.AddTransient<GitHubService>();
    })
    .Build();

try
{
    var gitHubService = host.Services.GetRequiredService<GitHubService>();
    var gitHubBranches = await gitHubService.GetAspNetCoreDocsBranchesAsync();

    Console.WriteLine($"{gitHubBranches?.Count() ?? 0} GitHub Branches");

    if (gitHubBranches is not null)
    {
        foreach (var gitHubBranch in gitHubBranches)
        {
            Console.WriteLine($"- {gitHubBranch.Name}");
        }
    }
}
catch (Exception ex)
{
    host.Services.GetRequiredService<ILogger<Program>>()
        .LogError(ex, "Unable to load branches from GitHub.");
}

public class GitHubService
{
    private readonly IHttpClientFactory _httpClientFactory;

    public GitHubService(IHttpClientFactory httpClientFactory) =>
        _httpClientFactory = httpClientFactory;

    public async Task<IEnumerable<GitHubBranch>?> GetAspNetCoreDocsBranchesAsync()
    {
        var httpRequestMessage = new HttpRequestMessage(
            HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches")
        {
            Headers =
            {
                { "Accept", "application/vnd.github.v3+json" },
                { "User-Agent", "HttpRequestsConsoleSample" }
            }
        };

        var httpClient = _httpClientFactory.CreateClient();
        var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage);

        httpResponseMessage.EnsureSuccessStatusCode();

        using var contentStream =
            await httpResponseMessage.Content.ReadAsStreamAsync();
        
        return await JsonSerializer.DeserializeAsync
            <IEnumerable<GitHubBranch>>(contentStream);
    }
}

public record GitHubBranch(
    [property: JsonPropertyName("name")] string Name);

Middleware de propagación de encabezados

La propagación de encabezados es un middleware de ASP.NET Core que se usa para propagar encabezados HTTP de la solicitud entrante a las solicitudes HttpClient salientes. Para utilizar la propagación de encabezados:

  • Instale el paquete Microsoft.AspNetCore.HeaderPropagation.

  • Configure HttpClient y la canalización de middleware en Program.cs:

    // Add services to the container.
    builder.Services.AddControllers();
    
    builder.Services.AddHttpClient("PropagateHeaders")
        .AddHeaderPropagation();
    
    builder.Services.AddHeaderPropagation(options =>
    {
        options.Headers.Add("X-TraceId");
    });
    
    var app = builder.Build();
    
    // Configure the HTTP request pipeline.
    app.UseHttpsRedirection();
    
    app.UseHeaderPropagation();
    
    app.MapControllers();
    
  • Realice solicitudes salientes mediante la instancia de HttpClient configurada, que incluye los encabezados agregados.

Recursos adicionales

Por Kirk Larkin, Steve Gordon, Glenn Condron y Ryan Nowak.

Se puede registrar y usar una interfaz IHttpClientFactory para crear y configurar instancias de HttpClient en una aplicación. IHttpClientFactory ofrece las ventajas siguientes:

  • Proporciona una ubicación central para denominar y configurar instancias de HttpClient lógicas. Por ejemplo, se podría registrar un cliente llamado github y configurarlo para acceder a GitHub. Se puede registrar un cliente predeterminado para el acceso general.
  • Codifica el concepto de middleware de salida a través de la delegación de controladores en HttpClient. Proporciona extensiones para el middleware basado en Polly a fin de aprovechar los controladores de delegación en HttpClient.
  • Administra la agrupación y la duración de las instancias de HttpClientMessageHandler subyacentes. La administración automática evita los problemas comunes de DNS (Sistema de nombres de dominio) 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.

Vea o descargue el código de ejemplo (cómo descargarlo).

En el código de ejemplo de la versión este tema se usa System.Text.Json para deserializar el contenido JSON devuelto en las respuestas HTTP. Para obtener ejemplos en los que se usan Json.NET y ReadAsAsync<T>, utilice el selector de versión para seleccionar una versión 2.x de este tema.

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

IHttpClientFactory se puede registrar mediante una llamada a AddHttpClient:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHttpClient();
        // Remaining code deleted for brevity.

Se puede solicitar una instancia de IHttpClientFactory mediante la inserción de dependencias (DI). En el código siguiente se usa IHttpClientFactory para crear una instancia de HttpClient:

public class BasicUsageModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubBranch> Branches { get; private set; }

    public bool GetBranchesError { get; private set; }

    public BasicUsageModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches");
        request.Headers.Add("Accept", "application/vnd.github.v3+json");
        request.Headers.Add("User-Agent", "HttpClientFactory-Sample");

        var client = _clientFactory.CreateClient();

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            Branches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(responseStream);
        }
        else
        {
            GetBranchesError = true;
            Branches = Array.Empty<GitHubBranch>();
        }
    }
}

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 HttpClient de tienen otra configuración.

La configuración de un objeto HttpClient con nombre se puede realizar durante la fase de registro en Startup.ConfigureServices:

services.AddHttpClient("github", c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    // Github API versioning
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    // Github requires a user-agent
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

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

  • La dirección base. https://api.github.com/.
  • Dos encabezados necesarios para trabajar con la API de GitHub.

CreateClient

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:

public class NamedClientModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubPullRequest> PullRequests { get; private set; }

    public bool GetPullRequestsError { get; private set; }

    public bool HasPullRequests => PullRequests.Any();

    public NamedClientModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "repos/dotnet/AspNetCore.Docs/pulls");

        var client = _clientFactory.CreateClient("github");

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            PullRequests = await JsonSerializer.DeserializeAsync
                    <IEnumerable<GitHubPullRequest>>(responseStream);
        }
        else
        {
            GetPullRequestsError = true;
            PullRequests = Array.Empty<GitHubPullRequest>();
        }
    }
}

En el código anterior, no es necesario especificar un nombre de host en la solicitud. 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.
  • Ofrecen 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:

public class GitHubService
{
    public HttpClient Client { get; }

    public GitHubService(HttpClient client)
    {
        client.BaseAddress = new Uri("https://api.github.com/");
        // GitHub API versioning
        client.DefaultRequestHeaders.Add("Accept",
            "application/vnd.github.v3+json");
        // GitHub requires a user-agent
        client.DefaultRequestHeaders.Add("User-Agent",
            "HttpClientFactory-Sample");

        Client = client;
    }

    public async Task<IEnumerable<GitHubIssue>> GetAspNetDocsIssues()
    {
        return await Client.GetFromJsonAsync<IEnumerable<GitHubIssue>>(
          "/repos/aspnet/AspNetCore.Docs/issues?state=open&sort=created&direction=desc");
    }
}

En el código anterior:

  • La configuración se mueve al cliente con tipo.
  • El objeto HttpClient se expone como una propiedad pública.

Se pueden crear métodos específicos de la API que exponen la funcionalidad de HttpClient. Por ejemplo, el método GetAspNetDocsIssues encapsula el código para recuperar incidencias abiertas.

En el código siguiente se llama a AddHttpClient en Startup.ConfigureServices para registrar una clase de cliente con tipo:

services.AddHttpClient<GitHubService>();

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

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

y se puede insertar y consumir directamente:

public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public IEnumerable<GitHubIssue> LatestIssues { get; private set; }

    public bool HasIssue => LatestIssues.Any();

    public bool GetIssuesError { get; private set; }

    public TypedClientModel(GitHubService gitHubService)
    {
        _gitHubService = gitHubService;
    }

    public async Task OnGet()
    {
        try
        {
            LatestIssues = await _gitHubService.GetAspNetDocsIssues();
        }
        catch(HttpRequestException)
        {
            GetIssuesError = true;
            LatestIssues = Array.Empty<GitHubIssue>();
        }
    }
}

La configuración de un cliente con tipo se puede especificar durante su registro en Startup.ConfigureServices, en lugar de en su constructor:

services.AddHttpClient<RepoService>(c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

HttpClient se puede encapsular dentro de un cliente con tipo. En lugar de exponerlo como una propiedad, defina un método que llame a la instancia de HttpClient internamente:

public class RepoService
{
    // _httpClient isn't exposed publicly
    private readonly HttpClient _httpClient;

    public RepoService(HttpClient client)
    {
        _httpClient = client;
    }

    public async Task<IEnumerable<string>> GetRepos()
    {
        var response = await _httpClient.GetAsync("aspnet/repos");

        response.EnsureSuccessStatusCode();

        using var responseStream = await response.Content.ReadAsStreamAsync();
        return await JsonSerializer.DeserializeAsync
            <IEnumerable<string>>(responseStream);
    }
}

En el código anterior, HttpClient se almacena en un campo privado. El acceso a HttpClient se realiza mediante el método GetRepos público.

Clientes generados

IHttpClientFactory se puede usar en combinación con bibliotecas de terceros, como Refit. Refit es una biblioteca de REST para .NET. Convierte las API de REST en interfaces en vivo. Se genera una implementación de la interfaz dinámicamente por medio de RestService, usando HttpClient para realizar las llamadas HTTP externas.

Se define una interfaz y una respuesta para representar la API externa y su correspondiente respuesta:

public interface IHelloClient
{
    [Get("/helloworld")]
    Task<Reply> GetMessageAsync();
}

public class Reply
{
    public string Message { get; set; }
}

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

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("hello", c =>
    {
        c.BaseAddress = new Uri("http://localhost:5000");
    })
    .AddTypedClient(c => Refit.RestService.For<IHelloClient>(c));

    services.AddControllers();
}

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

[ApiController]
public class ValuesController : ControllerBase
{
    private readonly IHelloClient _client;

    public ValuesController(IHelloClient client)
    {
        _client = client;
    }

    [HttpGet("/")]
    public async Task<ActionResult<Reply>> Index()
    {
        return await _client.GetMessageAsync();
    }
}

Realización de solicitudes POST, PUT y DELETE

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

  • POST
  • PUT
  • SUPRIMIR
  • PATCH

Para obtener una lista completa de los verbos HTTP admitidos, vea HttpMethod.

En el ejemplo siguiente se muestra cómo hacer una solicitud POST HTTP:

public async Task CreateItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem, _jsonSerializerOptions),
        Encoding.UTF8,
        "application/json");

    using var httpResponse =
        await _httpClient.PostAsync("/api/TodoItems", todoItemJson);

    httpResponse.EnsureSuccessStatusCode();
}

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

  • Serializa el parámetro TodoItem 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, consulta HttpContent.

En el ejemplo siguiente se muestra una solicitud PUT HTTP:

public async Task SaveItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem),
        Encoding.UTF8,
        "application/json");

    using var httpResponse =
        await _httpClient.PutAsync($"/api/TodoItems/{todoItem.Id}", todoItemJson);

    httpResponse.EnsureSuccessStatusCode();
}

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

En el ejemplo siguiente se muestra una solicitud DELETE HTTP:

public async Task DeleteItemAsync(long itemId)
{
    using var httpResponse =
        await _httpClient.DeleteAsync($"/api/TodoItems/{itemId}");

    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, consulta HttpClient.

Middleware de solicitud saliente

HttpClient tiene el concepto de controladores de delegación, que se pueden vincular entre sí para las solicitudes HTTP salientes. IHttpClientFactory:

  • simplifica la definición de controladores que se aplicarán por cada cliente con nombre.
  • Admite el registro y encadenamiento de varios controladores para crear una canalización de middleware de solicitud saliente. Cada uno de estos controladores es capaz de realizar la tarea antes y después de la solicitud de salida. Este patrón:
    • Es similar a la canalización de middleware de entrada de ASP.NET Core.
    • Proporciona un mecanismo para administrar los intereses transversales relacionados con las solicitudes HTTP, como:
      • el almacenamiento en caché
      • el control de errores
      • la serialización
      • el registro

Para crear un controlador de delegación:

  • Derívalo de DelegatingHandler.
  • Reemplaza SendAsync. Ejecuta el código antes de pasar la solicitud al siguiente controlador de la canalización:
public class ValidateHeaderHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains("X-API-KEY"))
        {
            return new HttpResponseMessage(HttpStatusCode.BadRequest)
            {
                Content = new StringContent(
                    "You must supply an API key header called X-API-KEY")
            };
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

El código anterior comprueba si el encabezado X-API-KEY está en la solicitud. Si falta X-API-KEY, se devuelve BadRequest.

Se puede agregar más de un controlador a la configuración de una instancia de HttpClient con Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessageHandler:

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<ValidateHeaderHandler>();

    services.AddHttpClient("externalservice", c =>
    {
        // Assume this is an "external" service which requires an API KEY
        c.BaseAddress = new Uri("https://localhost:5001/");
    })
    .AddHttpMessageHandler<ValidateHeaderHandler>();

    // Remaining code deleted for brevity.

En el código anterior, ValidateHeaderHandler se ha registrado con inserción de dependencias. Una vez registrado, se puede llamar a AddHttpMessageHandler, pasando el tipo del controlador.

Se pueden registrar varios controladores en el orden en que deben ejecutarse. Cada controlador contiene el siguiente controlador hasta que el último HttpClientHandler ejecuta la solicitud:

services.AddTransient<SecureRequestHandler>();
services.AddTransient<RequestDataHandler>();

services.AddHttpClient("clientwithhandlers")
    // This handler is on the outside and called first during the 
    // request, last during the response.
    .AddHttpMessageHandler<SecureRequestHandler>()
    // This handler is on the inside, closest to the request being 
    // sent.
    .AddHttpMessageHandler<RequestDataHandler>();

Uso de la inserción de dependencias en el middleware de solicitud de salida

Cuando IHttpClientFactory crea un nuevo controlador de delegación, usa la inserción de dependencias para cumplir con los parámetros de constructor del controlador. IHttpClientFactory crea un ámbito de inserción de dependencias independiente para cada controlador, lo que puede provocar un comportamiento sorprendente cuando un controlador consume un servicio con ámbito.

Por ejemplo, considera la siguiente interfaz y su implementación, que representa una tarea como una operación con un identificador, OperationId:

public interface IOperationScoped 
{
    string OperationId { get; }
}

public class OperationScoped : IOperationScoped
{
    public string OperationId { get; } = Guid.NewGuid().ToString()[^4..];
}

Como sugiere su nombre, IOperationScoped se registra con la inserción de dependencias mediante una duración con ámbito:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<TodoContext>(options =>
        options.UseInMemoryDatabase("TodoItems"));

    services.AddHttpContextAccessor();

    services.AddHttpClient<TodoClient>((sp, httpClient) =>
    {
        var httpRequest = sp.GetRequiredService<IHttpContextAccessor>().HttpContext.Request;

        // For sample purposes, assume TodoClient is used in the context of an incoming request.
        httpClient.BaseAddress = new Uri(UriHelper.BuildAbsolute(httpRequest.Scheme,
                                         httpRequest.Host, httpRequest.PathBase));
        httpClient.Timeout = TimeSpan.FromSeconds(5);
    });

    services.AddScoped<IOperationScoped, OperationScoped>();
    
    services.AddTransient<OperationHandler>();
    services.AddTransient<OperationResponseHandler>();

    services.AddHttpClient("Operation")
        .AddHttpMessageHandler<OperationHandler>()
        .AddHttpMessageHandler<OperationResponseHandler>()
        .SetHandlerLifetime(TimeSpan.FromSeconds(5));

    services.AddControllers();
    services.AddRazorPages();
}

El siguiente controlador de delegación consume y usa IOperationScoped para establecer el encabezado X-OPERATION-ID para la solicitud de salida:

public class OperationHandler : DelegatingHandler
{
    private readonly IOperationScoped _operationService;

    public OperationHandler(IOperationScoped operationScoped)
    {
        _operationService = operationScoped;
    }

    protected async override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Headers.Add("X-OPERATION-ID", _operationService.OperationId);

        return await base.SendAsync(request, cancellationToken);
    }
}

En la descarga de HttpRequestsSample, vaya a /Operation y actualice la página. El valor del ámbito de la solicitud cambia para cada solicitud, pero el valor del ámbito del controlador solo cambia cada 5 segundos.

Los controladores pueden depender de servicios de cualquier ámbito. Los servicios de los que dependen los controladores se eliminan cuando se elimina el controlador.

Usa uno de los siguientes enfoques para compartir el estado por solicitud con controladores de mensajes:

Usar controladores basados en Polly

IHttpClientFactory se integra con la biblioteca de terceros Polly. Polly es una biblioteca con capacidades de resistencia y control de errores transitorios para .NET. Permite a los desarrolladores expresar directivas como, por ejemplo, de reintento, interruptor, tiempo de espera, aislamiento compartimentado y reserva de forma fluida y segura para los subprocesos.

Se proporcionan métodos de extensión para hacer posible el uso de directivas de Polly con instancias de HttpClient configuradas. Las extensiones de Polly permiten agregar controladores basados en Polly a los clientes. Polly requiere el paquete NuGet Microsoft.Extensions.Http.Polly.

Control de errores transitorios

Los errores se suelen producir cuando las llamadas HTTP externas son transitorias. AddTransientHttpErrorPolicy permite definir una directiva para controlar los errores transitorios. Las directivas configuradas con AddTransientHttpErrorPolicy controlan las respuestas siguientes:

AddTransientHttpErrorPolicy proporciona acceso a un objeto PolicyBuilder configurado para controlar los errores que representan un posible error transitorio:

public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpClient<UnreliableEndpointCallerService>()
        .AddTransientHttpErrorPolicy(p => 
            p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));

    // Remaining code deleted for brevity.

En el código anterior, se define una directiva WaitAndRetryAsync. Las solicitudes erróneas se reintentan hasta tres veces con un retardo de 600 ms entre intentos.

Seleccionar directivas dinámicamente

Los métodos de extensión se proporcionan para agregar controladores basados en Polly, por ejemplo, AddPolicyHandler. La siguiente sobrecarga de AddPolicyHandler inspecciona la solicitud para decidir qué directiva se debe aplicar:

var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

services.AddHttpClient("conditionalpolicy")
// Run some code to select a policy based on the request
    .AddPolicyHandler(request => 
        request.Method == HttpMethod.Get ? timeout : longTimeout);

En el código anterior, si la solicitud GET saliente es del tipo HTTP, se aplica un tiempo de espera de 10 segundos. En cualquier otro método HTTP, se usa un tiempo de espera de 30 segundos.

Agregar varios controladores de Polly

Es común anidar las directivas de Polly:

services.AddHttpClient("multiplepolicies")
    .AddTransientHttpErrorPolicy(p => p.RetryAsync(3))
    .AddTransientHttpErrorPolicy(
        p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));

En el ejemplo anterior:

  • Se agregan dos controladores.
  • El primer controlador usa AddTransientHttpErrorPolicy para agregar una directiva de reintentos. Las solicitudes con error se reintentan hasta tres veces.
  • La segunda llamada a AddTransientHttpErrorPolicy agrega una directiva de interruptor. Las solicitudes externas adicionales se bloquean durante 30 segundos si se producen cinco intentos con error seguidos. Las directivas de interruptor tienen estado. Así, todas las llamadas realizadas a través de este cliente comparten el mismo estado de circuito.

Agregar directivas desde el Registro de Polly

Una forma de administrar las directivas usadas habitualmente consiste en definirlas una vez y registrarlas con PolicyRegistry.

En el código siguiente:

public void ConfigureServices(IServiceCollection services)
{           
    var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(10));
    var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(30));
    
    var registry = services.AddPolicyRegistry();

    registry.Add("regular", timeout);
    registry.Add("long", longTimeout);
    
    services.AddHttpClient("regularTimeoutHandler")
        .AddPolicyHandlerFromRegistry("regular");

    services.AddHttpClient("longTimeoutHandler")
       .AddPolicyHandlerFromRegistry("long");

    // Remaining code deleted for brevity.

Para más información sobre IHttpClientFactory y las integraciones de Polly, vea la wiki de Polly.

HttpClient y administración de la duración

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

IHttpClientFactory agrupa las instancias de HttpMessageHandler creadas por Factory para reducir el consumo de recursos. Se puede reutilizar una instancia de HttpMessageHandler del grupo al crear una instancia de HttpClient si su duración aún no ha expirado.

Se recomienda agrupar controladores porque cada uno de ellos normalmente administra sus propias conexiones HTTP subyacentes. Crear más controladores de los necesarios puede provocar retrasos en la conexión. Además, algunos controladores dejan las conexiones abiertas de forma indefinida, lo que puede impedir que el controlador reaccione ante los cambios de DNS (Sistema de nombres de dominio).

La duración de controlador predeterminada es dos minutos. El valor predeterminado se puede reemplazar de forma individual en cada cliente con nombre:

public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpClient("extendedhandlerlifetime")
        .SetHandlerLifetime(TimeSpan.FromMinutes(5));

    // Remaining code deleted for brevity.

Normalmente, las instancias de HttpClient se pueden trata como objetos de .NET que no requieren eliminación. ya que se cancelan las solicitudes salientes y la instancia de HttpClient determinada no se puede usar después de llamar a Dispose. IHttpClientFactory realiza un seguimiento y elimina los recursos que usan las instancias de HttpClient.

Mantener una sola instancia de HttpClient activa durante un período prolongado es un patrón común que se utiliza antes de la concepción de IHttpClientFactory. Este patrón se convierte en innecesario tras la migración a IHttpClientFactory.

Alternativas a IHttpClientFactory

El uso de IHttpClientFactory en una aplicación habilitada para la inserción de dependencias evita lo siguiente:

  • Problemas de agotamiento de recursos mediante la agrupación de instancias de HttpMessageHandler.
  • Problemas de DNS obsoletos al recorrer las instancias de HttpMessageHandler a intervalos regulares.

Existen formas alternativas de solucionar los problemas anteriores mediante una instancia de SocketsHttpHandler de larga duración.

  • Cree una instancia de SocketsHttpHandler al iniciar la aplicación y úsela para la vida útil de la aplicación.
  • Configure PooledConnectionLifetime con un valor adecuado en función de los tiempos de actualización de DNS.
  • Cree instancias de HttpClient mediante new HttpClient(handler, disposeHandler: false) según sea necesario.

Los enfoques anteriores solucionan los problemas de administración de recursos que IHttpClientFactory resuelve de forma similar.

  • SocketsHttpHandler comparte las conexiones entre las instancias de HttpClient. Este uso compartido impide el agotamiento del socket.
  • SocketsHttpHandler recorre las conexiones según PooledConnectionLifetime para evitar problemas de DNS obsoletos.

Cookies

Las instancias de HttpMessageHandler agrupadas generan objetos CookieContainer que se comparten. El uso compartido de objetos CookieContainer no previsto suele generar código incorrecto. En el caso de las aplicaciones que requieren cookies, tenga en cuenta lo siguiente:

  • Deshabilitación del control automático de cookies
  • Evitar IHttpClientFactory

Llame a ConfigurePrimaryHttpMessageHandler para deshabilitar el control automático de cookies:

services.AddHttpClient("configured-disable-automatic-cookies")
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new HttpClientHandler()
        {
            UseCookies = false,
        };
    });

Registro

Los clientes que se han creado a través de IHttpClientFactory registran mensajes de registro de todas las solicitudes. Habilite el nivel de información adecuado en la configuración del registro para ver los mensajes de registro predeterminados. El registro de más información, como el registro de encabezados de solicitud, solo se incluye en el nivel de seguimiento.

La categoría de registro usada en cada cliente incluye el nombre del cliente. Un cliente denominado MyNamedClient, por ejemplo, registra mensajes con una categoría de "System.Net.Http.HttpClient.MyNamedClient.LogicalHandler". Los mensajes con el sufijo LogicalHandler se producen fuera de la canalización de controlador de la solicitud. En la solicitud, los mensajes se registran antes de que cualquier otro controlador de la canalización haya procesado la solicitud. En la respuesta, los mensajes se registran después de que cualquier otro controlador de la canalización haya recibido la respuesta.

El registro también se produce dentro de la canalización de controlador de la solicitud. En el ejemplo MyNamedClient, esos mensajes se registran con la categoría de registro "System.Net.Http.HttpClient.MyNamedClient.ClientHandler". En la solicitud, esto tiene lugar después de que todos los demás controladores se hayan ejecutado y justo antes de que se envíe la solicitud. En la respuesta, este registro incluye el estado de la respuesta antes de que vuelva a pasar por la canalización del controlador.

Al habilitar el registro tanto dentro como fuera de la canalización, se podrán inspeccionar los cambios realizados por otros controladores de la canalización. Esto puede incluir cambios en los encabezados de solicitud o en el código de estado de la respuesta.

La inclusión del nombre del cliente en la categoría de registro permite filtrar el registro para clientes con nombre específicos.

Configurar 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. Este delegado servirá para crear y configurar el elemento principal HttpMessageHandler que ese cliente usa:

public void ConfigureServices(IServiceCollection services)
{            
    services.AddHttpClient("configured-inner-handler")
        .ConfigurePrimaryHttpMessageHandler(() =>
        {
            return new HttpClientHandler()
            {
                AllowAutoRedirect = false,
                UseDefaultCredentials = true
            };
        });

    // Remaining code deleted for brevity.

Uso de IHttpClientFactory en una aplicación de consola

En una aplicación de consola, agregue las siguientes referencias de paquete al proyecto:

En el ejemplo siguiente:

  • IHttpClientFactory está registrado en el contenedor de servicios del host genérico.
  • MyService crea una instancia de generador de clientes a partir del servicio, que se usa para crear un elemento HttpClient. HttpClient se utiliza para recuperar una página web.
  • Main crea un ámbito para ejecutar el método GetPage del servicio y escribe los primeros 500 caracteres del contenido de la página web en la consola.
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
class Program
{
    static async Task<int> Main(string[] args)
    {
        var builder = new HostBuilder()
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHttpClient();
                services.AddTransient<IMyService, MyService>();
            }).UseConsoleLifetime();

        var host = builder.Build();

        try
        {
            var myService = host.Services.GetRequiredService<IMyService>();
            var pageContent = await myService.GetPage();

            Console.WriteLine(pageContent.Substring(0, 500));
        }
        catch (Exception ex)
        {
            var logger = host.Services.GetRequiredService<ILogger<Program>>();

            logger.LogError(ex, "An error occurred.");
        }

        return 0;
    }

    public interface IMyService
    {
        Task<string> GetPage();
    }

    public class MyService : IMyService
    {
        private readonly IHttpClientFactory _clientFactory;

        public MyService(IHttpClientFactory clientFactory)
        {
            _clientFactory = clientFactory;
        }

        public async Task<string> GetPage()
        {
            // Content from BBC One: Dr. Who website (©BBC)
            var request = new HttpRequestMessage(HttpMethod.Get,
                "https://www.bbc.co.uk/programmes/b006q2x0");
            var client = _clientFactory.CreateClient();
            var response = await client.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                return await response.Content.ReadAsStringAsync();
            }
            else
            {
                return $"StatusCode: {response.StatusCode}";
            }
        }
    }
}

Middleware de propagación de encabezados

La propagación de encabezados es un middleware ASP.NET Core que se usa para propagar encabezados HTTP de la solicitud entrante a las solicitudes de cliente HTTP salientes. Para utilizar la propagación de encabezados:

  • Haga referencia al paquete Microsoft.AspNetCore.HeaderPropagation.

  • Configure el middleware y HttpClient en Startup:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
    
        services.AddHttpClient("MyForwardingClient").AddHeaderPropagation();
        services.AddHeaderPropagation(options =>
        {
            options.Headers.Add("X-TraceId");
        });
    }
    
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
    
        app.UseHttpsRedirection();
    
        app.UseHeaderPropagation();
    
        app.UseRouting();
    
        app.UseAuthorization();
    
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
    
  • El cliente incluye los encabezados configurados en las solicitudes salientes:

    var client = clientFactory.CreateClient("MyForwardingClient");
    var response = client.GetAsync(...);
    

Recursos adicionales

Por Kirk Larkin, Steve Gordon, Glenn Condron y Ryan Nowak.

Se puede registrar y usar una interfaz IHttpClientFactory para crear y configurar instancias de HttpClient en una aplicación. IHttpClientFactory ofrece las ventajas siguientes:

  • Proporciona una ubicación central para denominar y configurar instancias de HttpClient lógicas. Por ejemplo, se podría registrar un cliente llamado github y configurarlo para acceder a GitHub. Se puede registrar un cliente predeterminado para el acceso general.
  • Codifica el concepto de middleware de salida a través de la delegación de controladores en HttpClient. Proporciona extensiones para el middleware basado en Polly a fin de aprovechar los controladores de delegación en HttpClient.
  • Administra la agrupación y la duración de las instancias de HttpClientMessageHandler subyacentes. La administración automática evita los problemas comunes de DNS (Sistema de nombres de dominio) 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.

Vea o descargue el código de ejemplo (cómo descargarlo).

En el código de ejemplo de la versión este tema se usa System.Text.Json para deserializar el contenido JSON devuelto en las respuestas HTTP. Para obtener ejemplos en los que se usan Json.NET y ReadAsAsync<T>, utilice el selector de versión para seleccionar una versión 2.x de este tema.

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

IHttpClientFactory se puede registrar mediante una llamada a AddHttpClient:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddHttpClient();
        // Remaining code deleted for brevity.

Se puede solicitar una instancia de IHttpClientFactory mediante la inserción de dependencias (DI). En el código siguiente se usa IHttpClientFactory para crear una instancia de HttpClient:

public class BasicUsageModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubBranch> Branches { get; private set; }

    public bool GetBranchesError { get; private set; }

    public BasicUsageModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches");
        request.Headers.Add("Accept", "application/vnd.github.v3+json");
        request.Headers.Add("User-Agent", "HttpClientFactory-Sample");

        var client = _clientFactory.CreateClient();

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            Branches = await JsonSerializer.DeserializeAsync
                <IEnumerable<GitHubBranch>>(responseStream);
        }
        else
        {
            GetBranchesError = true;
            Branches = Array.Empty<GitHubBranch>();
        }
    }
}

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 HttpClient de tienen otra configuración.

La configuración de un objeto HttpClient con nombre se puede realizar durante la fase de registro en Startup.ConfigureServices:

services.AddHttpClient("github", c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    // Github API versioning
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    // Github requires a user-agent
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

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

  • La dirección base. https://api.github.com/.
  • Dos encabezados necesarios para trabajar con la API de GitHub.

CreateClient

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:

public class NamedClientModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubPullRequest> PullRequests { get; private set; }

    public bool GetPullRequestsError { get; private set; }

    public bool HasPullRequests => PullRequests.Any();

    public NamedClientModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get,
            "repos/dotnet/AspNetCore.Docs/pulls");

        var client = _clientFactory.CreateClient("github");

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            using var responseStream = await response.Content.ReadAsStreamAsync();
            PullRequests = await JsonSerializer.DeserializeAsync
                    <IEnumerable<GitHubPullRequest>>(responseStream);
        }
        else
        {
            GetPullRequestsError = true;
            PullRequests = Array.Empty<GitHubPullRequest>();
        }
    }
}

En el código anterior, no es necesario especificar un nombre de host en la solicitud. 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.
  • Ofrecen 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:

public class GitHubService
{
    public HttpClient Client { get; }

    public GitHubService(HttpClient client)
    {
        client.BaseAddress = new Uri("https://api.github.com/");
        // GitHub API versioning
        client.DefaultRequestHeaders.Add("Accept",
            "application/vnd.github.v3+json");
        // GitHub requires a user-agent
        client.DefaultRequestHeaders.Add("User-Agent",
            "HttpClientFactory-Sample");

        Client = client;
    }

    public async Task<IEnumerable<GitHubIssue>> GetAspNetDocsIssues()
    {
        var response = await Client.GetAsync(
            "/repos/dotnet/AspNetCore.Docs/issues?state=open&sort=created&direction=desc");

        response.EnsureSuccessStatusCode();

        using var responseStream = await response.Content.ReadAsStreamAsync();
        return await JsonSerializer.DeserializeAsync
            <IEnumerable<GitHubIssue>>(responseStream);
    }
}

Si quiere que los comentarios de código se traduzcan en más idiomas además del inglés, háganoslo saber en este problema de debate de GitHub.

En el código anterior:

  • La configuración se mueve al cliente con tipo.
  • El objeto HttpClient se expone como una propiedad pública.

Se pueden crear métodos específicos de la API que exponen la funcionalidad de HttpClient. Por ejemplo, el método GetAspNetDocsIssues encapsula el código para recuperar incidencias abiertas.

En el código siguiente se llama a AddHttpClient en Startup.ConfigureServices para registrar una clase de cliente con tipo:

services.AddHttpClient<GitHubService>();

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

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

y se puede insertar y consumir directamente:

public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public IEnumerable<GitHubIssue> LatestIssues { get; private set; }

    public bool HasIssue => LatestIssues.Any();

    public bool GetIssuesError { get; private set; }

    public TypedClientModel(GitHubService gitHubService)
    {
        _gitHubService = gitHubService;
    }

    public async Task OnGet()
    {
        try
        {
            LatestIssues = await _gitHubService.GetAspNetDocsIssues();
        }
        catch(HttpRequestException)
        {
            GetIssuesError = true;
            LatestIssues = Array.Empty<GitHubIssue>();
        }
    }
}

La configuración de un cliente con tipo se puede especificar durante su registro en Startup.ConfigureServices, en lugar de en su constructor:

services.AddHttpClient<RepoService>(c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

HttpClient se puede encapsular dentro de un cliente con tipo. En lugar de exponerlo como una propiedad, defina un método que llame a la instancia de HttpClient internamente:

public class RepoService
{
    // _httpClient isn't exposed publicly
    private readonly HttpClient _httpClient;

    public RepoService(HttpClient client)
    {
        _httpClient = client;
    }

    public async Task<IEnumerable<string>> GetRepos()
    {
        var response = await _httpClient.GetAsync("aspnet/repos");

        response.EnsureSuccessStatusCode();

        using var responseStream = await response.Content.ReadAsStreamAsync();
        return await JsonSerializer.DeserializeAsync
            <IEnumerable<string>>(responseStream);
    }
}

En el código anterior, HttpClient se almacena en un campo privado. El acceso a HttpClient se realiza mediante el método GetRepos público.

Clientes generados

IHttpClientFactory se puede usar en combinación con bibliotecas de terceros, como Refit. Refit es una biblioteca de REST para .NET. Convierte las API de REST en interfaces en vivo. Se genera una implementación de la interfaz dinámicamente por medio de RestService, usando HttpClient para realizar las llamadas HTTP externas.

Se define una interfaz y una respuesta para representar la API externa y su correspondiente respuesta:

public interface IHelloClient
{
    [Get("/helloworld")]
    Task<Reply> GetMessageAsync();
}

public class Reply
{
    public string Message { get; set; }
}

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

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("hello", c =>
    {
        c.BaseAddress = new Uri("http://localhost:5000");
    })
    .AddTypedClient(c => Refit.RestService.For<IHelloClient>(c));

    services.AddControllers();
}

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

[ApiController]
public class ValuesController : ControllerBase
{
    private readonly IHelloClient _client;

    public ValuesController(IHelloClient client)
    {
        _client = client;
    }

    [HttpGet("/")]
    public async Task<ActionResult<Reply>> Index()
    {
        return await _client.GetMessageAsync();
    }
}

Realización de solicitudes POST, PUT y DELETE

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

  • POST
  • PUT
  • SUPRIMIR
  • PATCH

Para obtener una lista completa de los verbos HTTP admitidos, vea HttpMethod.

En el ejemplo siguiente se muestra cómo hacer una solicitud POST HTTP:

public async Task CreateItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem, _jsonSerializerOptions),
        Encoding.UTF8,
        "application/json");

    using var httpResponse =
        await _httpClient.PostAsync("/api/TodoItems", todoItemJson);

    httpResponse.EnsureSuccessStatusCode();
}

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

  • Serializa el parámetro TodoItem 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, consulta HttpContent.

En el ejemplo siguiente se muestra una solicitud PUT HTTP:

public async Task SaveItemAsync(TodoItem todoItem)
{
    var todoItemJson = new StringContent(
        JsonSerializer.Serialize(todoItem),
        Encoding.UTF8,
        "application/json");

    using var httpResponse =
        await _httpClient.PutAsync($"/api/TodoItems/{todoItem.Id}", todoItemJson);

    httpResponse.EnsureSuccessStatusCode();
}

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

En el ejemplo siguiente se muestra una solicitud DELETE HTTP:

public async Task DeleteItemAsync(long itemId)
{
    using var httpResponse =
        await _httpClient.DeleteAsync($"/api/TodoItems/{itemId}");

    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, consulta HttpClient.

Middleware de solicitud saliente

HttpClient tiene el concepto de controladores de delegación, que se pueden vincular entre sí para las solicitudes HTTP salientes. IHttpClientFactory:

  • simplifica la definición de controladores que se aplicarán por cada cliente con nombre.
  • Admite el registro y encadenamiento de varios controladores para crear una canalización de middleware de solicitud saliente. Cada uno de estos controladores es capaz de realizar la tarea antes y después de la solicitud de salida. Este patrón:
    • Es similar a la canalización de middleware de entrada de ASP.NET Core.
    • Proporciona un mecanismo para administrar los intereses transversales relacionados con las solicitudes HTTP, como:
      • el almacenamiento en caché
      • el control de errores
      • la serialización
      • el registro

Para crear un controlador de delegación:

  • Derívalo de DelegatingHandler.
  • Reemplaza SendAsync. Ejecuta el código antes de pasar la solicitud al siguiente controlador de la canalización:
public class ValidateHeaderHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains("X-API-KEY"))
        {
            return new HttpResponseMessage(HttpStatusCode.BadRequest)
            {
                Content = new StringContent(
                    "You must supply an API key header called X-API-KEY")
            };
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

El código anterior comprueba si el encabezado X-API-KEY está en la solicitud. Si falta X-API-KEY, se devuelve BadRequest.

Se puede agregar más de un controlador a la configuración de una instancia de HttpClient con Microsoft.Extensions.DependencyInjection.HttpClientBuilderExtensions.AddHttpMessageHandler:

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<ValidateHeaderHandler>();

    services.AddHttpClient("externalservice", c =>
    {
        // Assume this is an "external" service which requires an API KEY
        c.BaseAddress = new Uri("https://localhost:5001/");
    })
    .AddHttpMessageHandler<ValidateHeaderHandler>();

    // Remaining code deleted for brevity.

En el código anterior, ValidateHeaderHandler se ha registrado con inserción de dependencias. Una vez registrado, se puede llamar a AddHttpMessageHandler, pasando el tipo del controlador.

Se pueden registrar varios controladores en el orden en que deben ejecutarse. Cada controlador contiene el siguiente controlador hasta que el último HttpClientHandler ejecuta la solicitud:

services.AddTransient<SecureRequestHandler>();
services.AddTransient<RequestDataHandler>();

services.AddHttpClient("clientwithhandlers")
    // This handler is on the outside and called first during the 
    // request, last during the response.
    .AddHttpMessageHandler<SecureRequestHandler>()
    // This handler is on the inside, closest to the request being 
    // sent.
    .AddHttpMessageHandler<RequestDataHandler>();

Uso de la inserción de dependencias en el middleware de solicitud de salida

Cuando IHttpClientFactory crea un nuevo controlador de delegación, usa la inserción de dependencias para cumplir con los parámetros de constructor del controlador. IHttpClientFactory crea un ámbito de inserción de dependencias independiente para cada controlador, lo que puede provocar un comportamiento sorprendente cuando un controlador consume un servicio con ámbito.

Por ejemplo, considera la siguiente interfaz y su implementación, que representa una tarea como una operación con un identificador, OperationId:

public interface IOperationScoped 
{
    string OperationId { get; }
}

public class OperationScoped : IOperationScoped
{
    public string OperationId { get; } = Guid.NewGuid().ToString()[^4..];
}

Como sugiere su nombre, IOperationScoped se registra con la inserción de dependencias mediante una duración con ámbito:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<TodoContext>(options =>
        options.UseInMemoryDatabase("TodoItems"));

    services.AddHttpContextAccessor();

    services.AddHttpClient<TodoClient>((sp, httpClient) =>
    {
        var httpRequest = sp.GetRequiredService<IHttpContextAccessor>().HttpContext.Request;

        // For sample purposes, assume TodoClient is used in the context of an incoming request.
        httpClient.BaseAddress = new Uri(UriHelper.BuildAbsolute(httpRequest.Scheme,
                                         httpRequest.Host, httpRequest.PathBase));
        httpClient.Timeout = TimeSpan.FromSeconds(5);
    });

    services.AddScoped<IOperationScoped, OperationScoped>();
    
    services.AddTransient<OperationHandler>();
    services.AddTransient<OperationResponseHandler>();

    services.AddHttpClient("Operation")
        .AddHttpMessageHandler<OperationHandler>()
        .AddHttpMessageHandler<OperationResponseHandler>()
        .SetHandlerLifetime(TimeSpan.FromSeconds(5));

    services.AddControllers();
    services.AddRazorPages();
}

El siguiente controlador de delegación consume y usa IOperationScoped para establecer el encabezado X-OPERATION-ID para la solicitud de salida:

public class OperationHandler : DelegatingHandler
{
    private readonly IOperationScoped _operationService;

    public OperationHandler(IOperationScoped operationScoped)
    {
        _operationService = operationScoped;
    }

    protected async override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Headers.Add("X-OPERATION-ID", _operationService.OperationId);

        return await base.SendAsync(request, cancellationToken);
    }
}

En la descarga de HttpRequestsSample, vaya a /Operation y actualice la página. El valor del ámbito de la solicitud cambia para cada solicitud, pero el valor del ámbito del controlador solo cambia cada 5 segundos.

Los controladores pueden depender de servicios de cualquier ámbito. Los servicios de los que dependen los controladores se eliminan cuando se elimina el controlador.

Usa uno de los siguientes enfoques para compartir el estado por solicitud con controladores de mensajes:

Usar controladores basados en Polly

IHttpClientFactory se integra con la biblioteca de terceros Polly. Polly es una biblioteca con capacidades de resistencia y control de errores transitorios para .NET. Permite a los desarrolladores expresar directivas como, por ejemplo, de reintento, interruptor, tiempo de espera, aislamiento compartimentado y reserva de forma fluida y segura para los subprocesos.

Se proporcionan métodos de extensión para hacer posible el uso de directivas de Polly con instancias de HttpClient configuradas. Las extensiones de Polly permiten agregar controladores basados en Polly a los clientes. Polly requiere el paquete NuGet Microsoft.Extensions.Http.Polly.

Control de errores transitorios

Los errores se suelen producir cuando las llamadas HTTP externas son transitorias. AddTransientHttpErrorPolicy permite definir una directiva para controlar los errores transitorios. Las directivas configuradas con AddTransientHttpErrorPolicy controlan las respuestas siguientes:

AddTransientHttpErrorPolicy proporciona acceso a un objeto PolicyBuilder configurado para controlar los errores que representan un posible error transitorio:

public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpClient<UnreliableEndpointCallerService>()
        .AddTransientHttpErrorPolicy(p => 
            p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));

    // Remaining code deleted for brevity.

En el código anterior, se define una directiva WaitAndRetryAsync. Las solicitudes erróneas se reintentan hasta tres veces con un retardo de 600 ms entre intentos.

Seleccionar directivas dinámicamente

Los métodos de extensión se proporcionan para agregar controladores basados en Polly, por ejemplo, AddPolicyHandler. La siguiente sobrecarga de AddPolicyHandler inspecciona la solicitud para decidir qué directiva se debe aplicar:

var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

services.AddHttpClient("conditionalpolicy")
// Run some code to select a policy based on the request
    .AddPolicyHandler(request => 
        request.Method == HttpMethod.Get ? timeout : longTimeout);

En el código anterior, si la solicitud GET saliente es del tipo HTTP, se aplica un tiempo de espera de 10 segundos. En cualquier otro método HTTP, se usa un tiempo de espera de 30 segundos.

Agregar varios controladores de Polly

Es común anidar las directivas de Polly:

services.AddHttpClient("multiplepolicies")
    .AddTransientHttpErrorPolicy(p => p.RetryAsync(3))
    .AddTransientHttpErrorPolicy(
        p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));

En el ejemplo anterior:

  • Se agregan dos controladores.
  • El primer controlador usa AddTransientHttpErrorPolicy para agregar una directiva de reintentos. Las solicitudes con error se reintentan hasta tres veces.
  • La segunda llamada a AddTransientHttpErrorPolicy agrega una directiva de interruptor. Las solicitudes externas adicionales se bloquean durante 30 segundos si se producen cinco intentos con error seguidos. Las directivas de interruptor tienen estado. Así, todas las llamadas realizadas a través de este cliente comparten el mismo estado de circuito.

Agregar directivas desde el Registro de Polly

Una forma de administrar las directivas usadas habitualmente consiste en definirlas una vez y registrarlas con PolicyRegistry.

En el código siguiente:

public void ConfigureServices(IServiceCollection services)
{           
    var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(10));
    var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
        TimeSpan.FromSeconds(30));
    
    var registry = services.AddPolicyRegistry();

    registry.Add("regular", timeout);
    registry.Add("long", longTimeout);
    
    services.AddHttpClient("regularTimeoutHandler")
        .AddPolicyHandlerFromRegistry("regular");

    services.AddHttpClient("longTimeoutHandler")
       .AddPolicyHandlerFromRegistry("long");

    // Remaining code deleted for brevity.

Para más información sobre IHttpClientFactory y las integraciones de Polly, vea la wiki de Polly.

HttpClient y administración de la duración

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

IHttpClientFactory agrupa las instancias de HttpMessageHandler creadas por Factory para reducir el consumo de recursos. Se puede reutilizar una instancia de HttpMessageHandler del grupo al crear una instancia de HttpClient si su duración aún no ha expirado.

Se recomienda agrupar controladores porque cada uno de ellos normalmente administra sus propias conexiones HTTP subyacentes. Crear más controladores de los necesarios puede provocar retrasos en la conexión. Además, algunos controladores dejan las conexiones abiertas de forma indefinida, lo que puede impedir que el controlador reaccione ante los cambios de DNS (Sistema de nombres de dominio).

La duración de controlador predeterminada es dos minutos. El valor predeterminado se puede reemplazar de forma individual en cada cliente con nombre:

public void ConfigureServices(IServiceCollection services)
{           
    services.AddHttpClient("extendedhandlerlifetime")
        .SetHandlerLifetime(TimeSpan.FromMinutes(5));

    // Remaining code deleted for brevity.

Normalmente, las instancias de HttpClient se pueden trata como objetos de .NET que no requieren eliminación. ya que se cancelan las solicitudes salientes y la instancia de HttpClient determinada no se puede usar después de llamar a Dispose. IHttpClientFactory realiza un seguimiento y elimina los recursos que usan las instancias de HttpClient.

Mantener una sola instancia de HttpClient activa durante un período prolongado es un patrón común que se utiliza antes de la concepción de IHttpClientFactory. Este patrón se convierte en innecesario tras la migración a IHttpClientFactory.

Alternativas a IHttpClientFactory

El uso de IHttpClientFactory en una aplicación habilitada para la inserción de dependencias evita lo siguiente:

  • Problemas de agotamiento de recursos mediante la agrupación de instancias de HttpMessageHandler.
  • Problemas de DNS obsoletos al recorrer las instancias de HttpMessageHandler a intervalos regulares.

Existen formas alternativas de solucionar los problemas anteriores mediante una instancia de SocketsHttpHandler de larga duración.

  • Cree una instancia de SocketsHttpHandler al iniciar la aplicación y úsela para la vida útil de la aplicación.
  • Configure PooledConnectionLifetime con un valor adecuado en función de los tiempos de actualización de DNS.
  • Cree instancias de HttpClient mediante new HttpClient(handler, disposeHandler: false) según sea necesario.

Los enfoques anteriores solucionan los problemas de administración de recursos que IHttpClientFactory resuelve de forma similar.

  • SocketsHttpHandler comparte las conexiones entre las instancias de HttpClient. Este uso compartido impide el agotamiento del socket.
  • SocketsHttpHandler recorre las conexiones según PooledConnectionLifetime para evitar problemas de DNS obsoletos.

Cookies

Las instancias de HttpMessageHandler agrupadas generan objetos CookieContainer que se comparten. El uso compartido de objetos CookieContainer no previsto suele generar código incorrecto. En el caso de las aplicaciones que requieren cookies, tenga en cuenta lo siguiente:

  • Deshabilitación del control automático de cookies
  • Evitar IHttpClientFactory

Llame a ConfigurePrimaryHttpMessageHandler para deshabilitar el control automático de cookies:

services.AddHttpClient("configured-disable-automatic-cookies")
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new HttpClientHandler()
        {
            UseCookies = false,
        };
    });

Registro

Los clientes que se han creado a través de IHttpClientFactory registran mensajes de registro de todas las solicitudes. Habilite el nivel de información adecuado en la configuración del registro para ver los mensajes de registro predeterminados. El registro de más información, como el registro de encabezados de solicitud, solo se incluye en el nivel de seguimiento.

La categoría de registro usada en cada cliente incluye el nombre del cliente. Un cliente denominado MyNamedClient, por ejemplo, registra mensajes con una categoría de "System.Net.Http.HttpClient.MyNamedClient.LogicalHandler". Los mensajes con el sufijo LogicalHandler se producen fuera de la canalización de controlador de la solicitud. En la solicitud, los mensajes se registran antes de que cualquier otro controlador de la canalización haya procesado la solicitud. En la respuesta, los mensajes se registran después de que cualquier otro controlador de la canalización haya recibido la respuesta.

El registro también se produce dentro de la canalización de controlador de la solicitud. En el ejemplo MyNamedClient, esos mensajes se registran con la categoría de registro "System.Net.Http.HttpClient.MyNamedClient.ClientHandler". En la solicitud, esto tiene lugar después de que todos los demás controladores se hayan ejecutado y justo antes de que se envíe la solicitud. En la respuesta, este registro incluye el estado de la respuesta antes de que vuelva a pasar por la canalización del controlador.

Al habilitar el registro tanto dentro como fuera de la canalización, se podrán inspeccionar los cambios realizados por otros controladores de la canalización. Esto puede incluir cambios en los encabezados de solicitud o en el código de estado de la respuesta.

La inclusión del nombre del cliente en la categoría de registro permite filtrar el registro para clientes con nombre específicos.

Configurar 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. Este delegado servirá para crear y configurar el elemento principal HttpMessageHandler que ese cliente usa:

public void ConfigureServices(IServiceCollection services)
{            
    services.AddHttpClient("configured-inner-handler")
        .ConfigurePrimaryHttpMessageHandler(() =>
        {
            return new HttpClientHandler()
            {
                AllowAutoRedirect = false,
                UseDefaultCredentials = true
            };
        });

    // Remaining code deleted for brevity.

Uso de IHttpClientFactory en una aplicación de consola

En una aplicación de consola, agregue las siguientes referencias de paquete al proyecto:

En el ejemplo siguiente:

  • IHttpClientFactory está registrado en el contenedor de servicios del host genérico.
  • MyService crea una instancia de generador de clientes a partir del servicio, que se usa para crear un elemento HttpClient. HttpClient se utiliza para recuperar una página web.
  • Main crea un ámbito para ejecutar el método GetPage del servicio y escribe los primeros 500 caracteres del contenido de la página web en la consola.
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
class Program
{
    static async Task<int> Main(string[] args)
    {
        var builder = new HostBuilder()
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHttpClient();
                services.AddTransient<IMyService, MyService>();
            }).UseConsoleLifetime();

        var host = builder.Build();

        try
        {
            var myService = host.Services.GetRequiredService<IMyService>();
            var pageContent = await myService.GetPage();

            Console.WriteLine(pageContent.Substring(0, 500));
        }
        catch (Exception ex)
        {
            var logger = host.Services.GetRequiredService<ILogger<Program>>();

            logger.LogError(ex, "An error occurred.");
        }

        return 0;
    }

    public interface IMyService
    {
        Task<string> GetPage();
    }

    public class MyService : IMyService
    {
        private readonly IHttpClientFactory _clientFactory;

        public MyService(IHttpClientFactory clientFactory)
        {
            _clientFactory = clientFactory;
        }

        public async Task<string> GetPage()
        {
            // Content from BBC One: Dr. Who website (©BBC)
            var request = new HttpRequestMessage(HttpMethod.Get,
                "https://www.bbc.co.uk/programmes/b006q2x0");
            var client = _clientFactory.CreateClient();
            var response = await client.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                return await response.Content.ReadAsStringAsync();
            }
            else
            {
                return $"StatusCode: {response.StatusCode}";
            }
        }
    }
}

Middleware de propagación de encabezados

La propagación de encabezados es un middleware ASP.NET Core que se usa para propagar encabezados HTTP de la solicitud entrante a las solicitudes de cliente HTTP salientes. Para utilizar la propagación de encabezados:

  • Haga referencia al paquete Microsoft.AspNetCore.HeaderPropagation.

  • Configure el middleware y HttpClient en Startup:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
    
        services.AddHttpClient("MyForwardingClient").AddHeaderPropagation();
        services.AddHeaderPropagation(options =>
        {
            options.Headers.Add("X-TraceId");
        });
    }
    
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
    
        app.UseHttpsRedirection();
    
        app.UseHeaderPropagation();
    
        app.UseRouting();
    
        app.UseAuthorization();
    
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
    
  • El cliente incluye los encabezados configurados en las solicitudes salientes:

    var client = clientFactory.CreateClient("MyForwardingClient");
    var response = client.GetAsync(...);
    

Recursos adicionales

Por Glenn Condron, Ryan Nowak y Steve Gordon

Se puede registrar y usar una interfaz IHttpClientFactory para crear y configurar instancias de HttpClient en una aplicación. Esto reporta las siguientes ventajas:

  • Proporciona una ubicación central para denominar y configurar instancias de HttpClient lógicas. Así, por ejemplo, se puede registrar y configurar un cliente github para tener acceso a GitHub. y, de igual modo, registrar otro cliente predeterminado para otros fines.
  • Codifica el concepto de middleware saliente a través de controladores de delegación en HttpClient y proporciona extensiones para middleware basado en Polly para poder sacar partido de este.
  • Administra la agrupación y duración de las instancias de HttpClientMessageHandler subyacentes para evitar los problemas de DNS que suelen producirse al administrar las duraciones de HttpClient manualmente.
  • 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.

Vea o descargue el código de ejemplo (cómo descargarlo)

Requisitos previos

Los proyectos para .NET Framework requieren instalar el paquete NuGet Microsoft.Extensions.Http. Los proyectos para .NET Core y que hagan referencia al metapaquete Microsoft.AspNetCore.App ya incluyen el paquete Microsoft.Extensions.Http.

Patrones de consumo

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

Ninguno de ellos es rigurosamente superior a otro, sino que el mejor método dependerá de las restricciones particulares de la aplicación.

Uso básico

Se puede registrar un IHttpClientFactory llamando al método de extensión AddHttpClient en IServiceCollection, dentro del método Startup.ConfigureServices.

services.AddHttpClient();

Una vez registrado, el código puede aceptar una interfaz IHttpClientFactory en cualquier parte donde se puedan insertar servicios por medio de la inserción de dependencias (DI). IHttpClientFactory se puede usar para crear una instancia de HttpClient:

public class BasicUsageModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubBranch> Branches { get; private set; }

    public bool GetBranchesError { get; private set; }

    public BasicUsageModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get, 
            "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches");
        request.Headers.Add("Accept", "application/vnd.github.v3+json");
        request.Headers.Add("User-Agent", "HttpClientFactory-Sample");

        var client = _clientFactory.CreateClient();

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            Branches = await response.Content
                .ReadAsAsync<IEnumerable<GitHubBranch>>();
        }
        else
        {
            GetBranchesError = true;
            Branches = Array.Empty<GitHubBranch>();
        }                               
    }
}

Cuando IHttpClientFactory se usa de este modo, constituye una excelente manera de refactorizar una aplicación existente. No tiene efecto alguno en la forma en que HttpClient se usa. En aquellos sitios en los que ya se hayan creado instancias de HttpClient, reemplace esas apariciones por una llamada a CreateClient.

Clientes con nombre

Si una aplicación necesita usar HttpClient de diversas maneras, cada una con una configuración diferente, una opción consiste en usar clientes con nombre. La configuración de un HttpClient con nombre se puede realizar durante la fase de registro en Startup.ConfigureServices.

services.AddHttpClient("github", c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    // Github API versioning
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    // Github requires a user-agent
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

En el código anterior, se llama a AddHttpClient usando el nombre github. Este cliente tiene aplicadas algunas configuraciones predeterminadas, a saber, la dirección base y dos encabezados necesarios para trabajar con la API de GitHub.

Cada vez que se llama a CreateClient, se crea otra instancia de HttpClient y se llama a la acción de configuración.

Para consumir un cliente con nombre, se puede pasar un parámetro de cadena a CreateClient. Especifique el nombre del cliente que se va a crear:

public class NamedClientModel : PageModel
{
    private readonly IHttpClientFactory _clientFactory;

    public IEnumerable<GitHubPullRequest> PullRequests { get; private set; }

    public bool GetPullRequestsError { get; private set; }

    public bool HasPullRequests => PullRequests.Any();

    public NamedClientModel(IHttpClientFactory clientFactory)
    {
        _clientFactory = clientFactory;
    }

    public async Task OnGet()
    {
        var request = new HttpRequestMessage(HttpMethod.Get, 
            "repos/dotnet/AspNetCore.Docs/pulls");

        var client = _clientFactory.CreateClient("github");

        var response = await client.SendAsync(request);

        if (response.IsSuccessStatusCode)
        {
            PullRequests = await response.Content
                .ReadAsAsync<IEnumerable<GitHubPullRequest>>();
        }
        else
        {
            GetPullRequestsError = true;
            PullRequests = Array.Empty<GitHubPullRequest>();
        }
    }
}

En el código anterior, no es necesario especificar un nombre de host en la solicitud. Basta con 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.
  • Ofrecen 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, el mismo cliente con tipo se puede usar para un punto de conexión back-end único y encapsular toda la lógica que se ocupa de ese 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:

public class GitHubService
{
    public HttpClient Client { get; }

    public GitHubService(HttpClient client)
    {
        client.BaseAddress = new Uri("https://api.github.com/");
        // GitHub API versioning
        client.DefaultRequestHeaders.Add("Accept", 
            "application/vnd.github.v3+json");
        // GitHub requires a user-agent
        client.DefaultRequestHeaders.Add("User-Agent", 
            "HttpClientFactory-Sample");

        Client = client;
    }

    public async Task<IEnumerable<GitHubIssue>> GetAspNetDocsIssues()
    {
        var response = await Client.GetAsync(
            "/repos/dotnet/AspNetCore.Docs/issues?state=open&sort=created&direction=desc");

        response.EnsureSuccessStatusCode();

        var result = await response.Content
            .ReadAsAsync<IEnumerable<GitHubIssue>>();

        return result;
    }
}

En el código anterior, la configuración se mueve al cliente con tipo. El objeto HttpClient se expone como una propiedad pública. Se pueden definir métodos específicos de API que exponen la funcionalidad HttpClient. El método GetAspNetDocsIssues encapsula el código necesario para consultar y analizar los últimos problemas abiertos de un repositorio de GitHub.

Para registrar un cliente con tipo, se puede usar el método de extensión genérico AddHttpClient dentro de Startup.ConfigureServices, especificando la clase del cliente con tipo:

services.AddHttpClient<GitHubService>();

El cliente con tipo se registra como transitorio con inserción con dependencias, y se puede insertar y consumir directamente:

public class TypedClientModel : PageModel
{
    private readonly GitHubService _gitHubService;

    public IEnumerable<GitHubIssue> LatestIssues { get; private set; }

    public bool HasIssue => LatestIssues.Any();

    public bool GetIssuesError { get; private set; }

    public TypedClientModel(GitHubService gitHubService)
    {
        _gitHubService = gitHubService;
    }

    public async Task OnGet()
    {
        try
        {
            LatestIssues = await _gitHubService.GetAspNetDocsIssues();
        }
        catch(HttpRequestException)
        {
            GetIssuesError = true;
            LatestIssues = Array.Empty<GitHubIssue>();
        }
    }
}

Si lo prefiere, la configuración de un cliente con nombre se puede especificar durante su registro en Startup.ConfigureServices, en lugar de en su constructor:

services.AddHttpClient<RepoService>(c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

El HttpClient se puede encapsular completamente dentro de un cliente con nombre. En lugar de exponerlo como una propiedad, se pueden proporcionar métodos públicos que llamen a la instancia de HttpClient internamente.

public class RepoService
{
    // _httpClient isn't exposed publicly
    private readonly HttpClient _httpClient;

    public RepoService(HttpClient client)
    {
        _httpClient = client;
    }

    public async Task<IEnumerable<string>> GetRepos()
    {
        var response = await _httpClient.GetAsync("aspnet/repos");

        response.EnsureSuccessStatusCode();

        var result = await response.Content
            .ReadAsAsync<IEnumerable<string>>();

        return result;
    }
}

En el código anterior, el HttpClient se almacena como un campo privado. Todo el acceso para realizar llamadas externas pasa por el método GetRepos.

Clientes generados

IHttpClientFactory se puede usar en combinación con otras bibliotecas de terceros, como Refit. Refit es una biblioteca de REST para .NET. Convierte las API de REST en interfaces en vivo. Se genera una implementación de la interfaz dinámicamente por medio de RestService, usando HttpClient para realizar las llamadas HTTP externas.

Se define una interfaz y una respuesta para representar la API externa y su correspondiente respuesta:

public interface IHelloClient
{
    [Get("/helloworld")]
    Task<Reply> GetMessageAsync();
}

public class Reply
{
    public string Message { get; set; }
}

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

public void ConfigureServices(IServiceCollection services)
{
    services.AddHttpClient("hello", c =>
    {
        c.BaseAddress = new Uri("http://localhost:5000");
    })
    .AddTypedClient(c => Refit.RestService.For<IHelloClient>(c));

    services.AddMvc();
}

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

[ApiController]
public class ValuesController : ControllerBase
{
    private readonly IHelloClient _client;

    public ValuesController(IHelloClient client)
    {
        _client = client;
    }

    [HttpGet("/")]
    public async Task<ActionResult<Reply>> Index()
    {
        return await _client.GetMessageAsync();
    }
}

Middleware de solicitud saliente

HttpClient ya posee el concepto de controladores de delegación, que se pueden vincular entre sí para las solicitudes HTTP salientes. IHttpClientFactory permite definir fácilmente los controladores que se usarán en cada cliente con nombre. Admite el registro y encadenamiento de varios controladores para crear una canalización de middleware de solicitud saliente. Cada uno de estos controladores es capaz de realizar la tarea antes y después de la solicitud de salida. Este patrón es similar a la canalización de middleware de entrada de ASP.NET Core. Dicho patrón proporciona un mecanismo para administrar cuestiones transversales relativas a las solicitudes HTTP, como el almacenamiento en caché, el control de errores, la serialización y el registro.

Para crear un controlador, defina una clase que se derive de DelegatingHandler. Invalide el método SendAsync para ejecutar el código antes de pasar la solicitud al siguiente controlador de la canalización:

public class ValidateHeaderHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (!request.Headers.Contains("X-API-KEY"))
        {
            return new HttpResponseMessage(HttpStatusCode.BadRequest)
            {
                Content = new StringContent(
                    "You must supply an API key header called X-API-KEY")
            };
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

El código anterior define un controlador básico. Comprueba si se ha incluido un encabezado X-API-KEY en la solicitud. Si no está presente, puede evitar la llamada HTTP y devolver una respuesta adecuada.

Durante el registro, se pueden agregar uno o varios controladores a la configuración de una instancia de HttpClient. Esta tarea se realiza a través de métodos de extensión en IHttpClientBuilder.

services.AddTransient<ValidateHeaderHandler>();

services.AddHttpClient("externalservice", c =>
{
    // Assume this is an "external" service which requires an API KEY
    c.BaseAddress = new Uri("https://localhost:5000/");
})
.AddHttpMessageHandler<ValidateHeaderHandler>();

En el código anterior, ValidateHeaderHandler se ha registrado con inserción de dependencias. El controlador debe estar registrado en la inserción de dependencias como servicio transitorio, nunca como servicio con ámbito. Si el controlador se registra como servicio con ámbito y se pueden eliminar los servicios de los que depende el controlador:

  • Los servicios del controlador se pueden eliminar antes de que el controlador deje de estar en el ámbito.
  • Los servicios del controlador que se eliminen provocarán un error en él.

Una vez registrado, se puede llamar a AddHttpMessageHandler, con lo que se pasa el tipo de controlador.

Se pueden registrar varios controladores en el orden en que deben ejecutarse. Cada controlador contiene el siguiente controlador hasta que el último HttpClientHandler ejecuta la solicitud:

services.AddTransient<SecureRequestHandler>();
services.AddTransient<RequestDataHandler>();

services.AddHttpClient("clientwithhandlers")
    // This handler is on the outside and called first during the 
    // request, last during the response.
    .AddHttpMessageHandler<SecureRequestHandler>()
    // This handler is on the inside, closest to the request being 
    // sent.
    .AddHttpMessageHandler<RequestDataHandler>();

Use uno de los siguientes enfoques para compartir el estado por solicitud con controladores de mensajes:

  • Pasa datos al controlador usando HttpRequestMessage.Properties.
  • Use IHttpContextAccessor para acceder a la solicitud actual.
  • Cree un objeto de almacenamiento AsyncLocal personalizado para pasar los datos.

Usar controladores basados en Polly

IHttpClientFactory se integra con una biblioteca de terceros muy conocida denominada Polly. Polly es una biblioteca con capacidades de resistencia y control de errores transitorios para .NET. Permite a los desarrolladores expresar directivas como, por ejemplo, de reintento, interruptor, tiempo de espera, aislamiento compartimentado y reserva de forma fluida y segura para los subprocesos.

Se proporcionan métodos de extensión para hacer posible el uso de directivas de Polly con instancias de HttpClient configuradas. Las extensiones de Polly:

  • permiten agregar controladores basados en Polly a los clientes;
  • se pueden usar tras instalar el paquete NuGet Microsoft.Extensions.Http.Polly, aunque este no está incluido en la plataforma compartida ASP.NET Core.

Control de errores transitorios

Los errores más comunes se producen cuando las llamadas HTTP externas son transitorias. Por ello, se incluye un método de extensión muy práctico denominado AddTransientHttpErrorPolicy, que permite definir una directiva para controlar los errores transitorios. Las directivas que se configuran con este método de extensión controlan HttpRequestException, las respuestas HTTP 5xx y las respuestas HTTP 408.

La extensión AddTransientHttpErrorPolicy se puede usar en Startup.ConfigureServices. La extensión da acceso a un objeto PolicyBuilder, configurado para controlar los errores que pueden constituir un posible error transitorio:

services.AddHttpClient<UnreliableEndpointCallerService>()
    .AddTransientHttpErrorPolicy(p => 
        p.WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(600)));

En el código anterior, se define una directiva WaitAndRetryAsync. Las solicitudes erróneas se reintentan hasta tres veces con un retardo de 600 ms entre intentos.

Seleccionar directivas dinámicamente

Existen más métodos de extensión que pueden servir para agregar controladores basados en Polly. Una de esas extensiones es AddPolicyHandler, que tiene varias sobrecargas. Una de esas sobrecargas permite inspeccionar la solicitud al dilucidar qué directiva aplicar:

var timeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(10));
var longTimeout = Policy.TimeoutAsync<HttpResponseMessage>(
    TimeSpan.FromSeconds(30));

services.AddHttpClient("conditionalpolicy")
// Run some code to select a policy based on the request
    .AddPolicyHandler(request => 
        request.Method == HttpMethod.Get ? timeout : longTimeout);

En el código anterior, si la solicitud GET saliente es del tipo HTTP, se aplica un tiempo de espera de 10 segundos. En cualquier otro método HTTP, se usa un tiempo de espera de 30 segundos.

Agregar varios controladores de Polly

Es habitual anidar directivas de Polly para proporcionar una funcionalidad mejorada:

services.AddHttpClient("multiplepolicies")
    .AddTransientHttpErrorPolicy(p => p.RetryAsync(3))
    .AddTransientHttpErrorPolicy(
        p => p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));

En el ejemplo anterior, se agregan dos controladores. En el primer ejemplo se usa la extensión AddTransientHttpErrorPolicy para agregar una directiva de reintento. Las solicitudes con error se reintentan hasta tres veces. La segunda llamada a AddTransientHttpErrorPolicy agrega una directiva de interruptor. Las solicitudes externas subsiguientes se bloquean durante 30 segundos si se producen cinco intentos infructuosos seguidos. Las directivas de interruptor tienen estado. Así, todas las llamadas realizadas a través de este cliente comparten el mismo estado de circuito.

Agregar directivas desde el Registro de Polly

Una forma de administrar las directivas usadas habitualmente consiste en definirlas una vez y registrarlas con PolicyRegistry. Se proporciona un método de extensión que permite agregar un controlador por medio de una directiva del Registro:

var registry = services.AddPolicyRegistry();

registry.Add("regular", timeout);
registry.Add("long", longTimeout);

services.AddHttpClient("regulartimeouthandler")
    .AddPolicyHandlerFromRegistry("regular");

En el código anterior, se registran dos directivas cuando se agrega PolicyRegistry a ServiceCollection. Para usar una directiva del Registro, se usa el método AddPolicyHandlerFromRegistry pasando el nombre de la directiva que se va a aplicar.

Encontrará más información sobre IHttpClientFactory y las integraciones de Polly en la wiki de Polly.

HttpClient y administración de la duración

Cada vez que se llama a CreateClient en IHttpClientFactory, se devuelve una nueva instancia de HttpClient. Hay un controlador HttpMessageHandler por cliente con nombre. La fábrica administra la duración de las instancias de HttpMessageHandler.

IHttpClientFactory agrupa las instancias de HttpMessageHandler creadas por Factory para reducir el consumo de recursos. Se puede reutilizar una instancia de HttpMessageHandler del grupo al crear una instancia de HttpClient si su duración aún no ha expirado.

Se recomienda agrupar controladores porque cada uno de ellos normalmente administra sus propias conexiones HTTP subyacentes. Crear más controladores de los necesarios 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.

La duración de controlador predeterminada es dos minutos. El valor predeterminado se puede reemplazar individualmente en cada cliente con nombre. Para ello, llame a SetHandlerLifetime en el IHttpClientBuilder que se devuelve cuando se crea el cliente:

services.AddHttpClient("extendedhandlerlifetime")
    .SetHandlerLifetime(TimeSpan.FromMinutes(5));

No hace falta eliminar el cliente, ya que se cancelan las solicitudes salientes y la instancia de HttpClient determinada no se puede usar después de llamar a Dispose. IHttpClientFactory realiza un seguimiento y elimina los recursos que usan las instancias de HttpClient. Normalmente, las instancias de HttpClient pueden tratarse como objetos de .NET que no requieren eliminación.

Mantener una sola instancia de HttpClient activa durante un período prolongado es un patrón común que se utiliza antes de la concepción de IHttpClientFactory. Este patrón se convierte en innecesario tras la migración a IHttpClientFactory.

Alternativas a IHttpClientFactory

El uso de IHttpClientFactory en una aplicación habilitada para la inserción de dependencias evita lo siguiente:

  • Problemas de agotamiento de recursos mediante la agrupación de instancias de HttpMessageHandler.
  • Problemas de DNS obsoletos al recorrer las instancias de HttpMessageHandler a intervalos regulares.

Existen formas alternativas de solucionar los problemas anteriores mediante una instancia de SocketsHttpHandler de larga duración.

  • Cree una instancia de SocketsHttpHandler al iniciar la aplicación y úsela para la vida útil de la aplicación.
  • Configure PooledConnectionLifetime con un valor adecuado en función de los tiempos de actualización de DNS.
  • Cree instancias de HttpClient mediante new HttpClient(handler, disposeHandler: false) según sea necesario.

Los enfoques anteriores solucionan los problemas de administración de recursos que IHttpClientFactory resuelve de forma similar.

  • SocketsHttpHandler comparte las conexiones entre las instancias de HttpClient. Este uso compartido impide el agotamiento del socket.
  • SocketsHttpHandler recorre las conexiones según PooledConnectionLifetime para evitar problemas de DNS obsoletos.

Cookies

Las instancias de HttpMessageHandler agrupadas generan objetos CookieContainer que se comparten. El uso compartido de objetos CookieContainer no previsto suele generar código incorrecto. En el caso de las aplicaciones que requieren cookies, tenga en cuenta lo siguiente:

  • Deshabilitación del control automático de cookies
  • Evitar IHttpClientFactory

Llame a ConfigurePrimaryHttpMessageHandler para deshabilitar el control automático de cookies:

services.AddHttpClient("configured-disable-automatic-cookies")
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new HttpClientHandler()
        {
            UseCookies = false,
        };
    });

Registro

Los clientes que se han creado a través de IHttpClientFactory registran mensajes de registro de todas las solicitudes. Habilite el nivel de información adecuado en la configuración del registro para ver los mensajes de registro predeterminados. El registro de más información, como el registro de encabezados de solicitud, solo se incluye en el nivel de seguimiento.

La categoría de registro usada en cada cliente incluye el nombre del cliente. Así, por ejemplo, un cliente llamado MyNamedClient registra mensajes con una categoría System.Net.Http.HttpClient.MyNamedClient.LogicalHandler. Los mensajes con el sufijo LogicalHandler se producen fuera de la canalización de controlador de la solicitud. En la solicitud, los mensajes se registran antes de que cualquier otro controlador de la canalización haya procesado la solicitud. En la respuesta, los mensajes se registran después de que cualquier otro controlador de la canalización haya recibido la respuesta.

El registro también se produce dentro de la canalización de controlador de la solicitud. En nuestro caso de ejemplo de MyNamedClient, esos mensajes se registran en la categoría de registro System.Net.Http.HttpClient.MyNamedClient.ClientHandler. En la solicitud, esto tiene lugar después de que todos los demás controladores se hayan ejecutado y justo antes de que la solicitud se envíe por la red. En la respuesta, este registro incluye el estado de la respuesta antes de que vuelva a pasar por la canalización del controlador.

Al habilitar el registro tanto dentro como fuera de la canalización, se podrán inspeccionar los cambios realizados por otros controladores de la canalización. Esto puede englobar cambios, por ejemplo, en los encabezados de solicitud o en el código de estado de la respuesta.

Si el nombre del cliente se incluye en la categoría de registro, dicho registro se podrá filtrar para encontrar clientes con nombre específicos cuando sea necesario.

Configurar 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. Este delegado servirá para crear y configurar el elemento principal HttpMessageHandler que ese cliente usa:

services.AddHttpClient("configured-inner-handler")
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        return new HttpClientHandler()
        {
            AllowAutoRedirect = false,
            UseDefaultCredentials = true
        };
    });

Uso de IHttpClientFactory en una aplicación de consola

En una aplicación de consola, agregue las siguientes referencias de paquete al proyecto:

En el ejemplo siguiente:

  • IHttpClientFactory está registrado en el contenedor de servicios del host genérico.
  • MyService crea una instancia de generador de clientes a partir del servicio, que se usa para crear un elemento HttpClient. HttpClient se utiliza para recuperar una página web.
  • El método GetPage del servicio se ejecuta para escribir los primeros 500 caracteres del contenido de la página web en la consola. Para más información sobre la llamada a servicios desde Program.Main, consulte Inserción de dependencias en ASP.NET Core.
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
class Program
{
    static async Task<int> Main(string[] args)
    {
        var builder = new HostBuilder()
            .ConfigureServices((hostContext, services) =>
            {
                services.AddHttpClient();
                services.AddTransient<IMyService, MyService>();
            }).UseConsoleLifetime();

        var host = builder.Build();

        try
        {
            var myService = host.Services.GetRequiredService<IMyService>();
            var pageContent = await myService.GetPage();

            Console.WriteLine(pageContent.Substring(0, 500));
        }
        catch (Exception ex)
        {
            var logger = host.Services.GetRequiredService<ILogger<Program>>();

            logger.LogError(ex, "An error occurred.");
        }

        return 0;
    }

    public interface IMyService
    {
        Task<string> GetPage();
    }

    public class MyService : IMyService
    {
        private readonly IHttpClientFactory _clientFactory;

        public MyService(IHttpClientFactory clientFactory)
        {
            _clientFactory = clientFactory;
        }

        public async Task<string> GetPage()
        {
            // Content from BBC One: Dr. Who website (©BBC)
            var request = new HttpRequestMessage(HttpMethod.Get,
                "https://www.bbc.co.uk/programmes/b006q2x0");
            var client = _clientFactory.CreateClient();
            var response = await client.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                return await response.Content.ReadAsStringAsync();
            }
            else
            {
                return $"StatusCode: {response.StatusCode}";
            }
        }
    }
}

Middleware de propagación de encabezados

La propagación de encabezado es un middleware compatible con la comunidad que se usa para propagar encabezados HTTP de la solicitud entrante a las solicitudes de cliente HTTP salientes. Para utilizar la propagación de encabezados:

  • Haga referencia al puerto de comunidad admitido del paquete HeaderPropagation. ASP.NET Core 3.1 y las versiones posteriores admiten Microsoft.AspNetCore.HeaderPropagation.

  • Configure el middleware y HttpClient en Startup:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    
        services.AddHttpClient("MyForwardingClient").AddHeaderPropagation();
        services.AddHeaderPropagation(options =>
        {
            options.Headers.Add("X-TraceId");
        });
    }
    
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseHsts();
        }
    
        app.UseHttpsRedirection();
    
        app.UseHeaderPropagation();
    
        app.UseMvc();
    }
    
  • El cliente incluye los encabezados configurados en las solicitudes salientes:

    var client = clientFactory.CreateClient("MyForwardingClient");
    var response = client.GetAsync(...);
    

Recursos adicionales