Realización de solicitudes HTTP mediante IHttpClientFactory en ASP.NET Core
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
HttpClientlógicas. Por ejemplo, se podría registrar un cliente 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 enHttpClient. - Administra la agrupación y la duración de las instancias de
HttpClientMessageHandlersubyacentes. La administración automática evita los problemas comunes de DNS (Sistema de nombres de dominio) que se producen al administrar la duración deHttpClientde 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
HttpClientde 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
HttpClientdeterminado 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
HttpClientse 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:
- Crea una instancia de
HttpClient. - Cree una instancia de
GitHubService, pasando la instancia deHttpClienta 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 que 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
TodoItemen JSON medianteSystem.Text.Json. Usa una instancia de JsonSerializerOptions para configurar el proceso de serialización. - Crea una instancia de StringContent a fin de empaquetar el JSON serializado para enviarlo en el cuerpo de la solicitud de HTTP.
- Llama a PostAsync para enviar el contenido de JSON a la URL especificada. Se trata de una URL relativa que se agrega a HttpClient.BaseAddress.
- Llama a EnsureSuccessStatusCode para iniciar una excepción si el código de estado de la respuesta no indica que la operación se ha procesado correctamente.
HttpClient también admite otros tipos de contenido. Por ejemplo, MultipartContent y StreamContent. Para obtener una lista completa del contenido admitido, vea HttpContent.
En el ejemplo siguiente se muestra una solicitud 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, vea 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 de salida. 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ívelo de DelegatingHandler.
- Reemplace SendAsync. Ejecute 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, considere 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.
Use uno de los siguientes enfoques para compartir el estado por solicitud con controladores de mensajes:
- Pase datos al controlador mediante HttpRequestMessage.Properties.
- Use IHttpContextAccessor para acceder a la solicitud actual.
- Cree un objeto de almacenamiento AsyncLocal<T> personalizado para pasar los datos.
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:
- HttpRequestException
- HTTP 5xx
- HTTP 408
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
AddTransientHttpErrorPolicyagrega 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:
- Se agregan las directivas "regular" y "long".
- AddPolicyHandlerFromRegistry agrega las directivas "regular" y "long" del registro.
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
HttpMessageHandlera intervalos regulares.
Existen formas alternativas de solucionar los problemas anteriores mediante una instancia de SocketsHttpHandler de larga duración.
- Cree una instancia de
SocketsHttpHandleral 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
HttpClientmediantenew 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.
SocketsHttpHandlercomparte las conexiones entre las instancias deHttpClient. Este uso compartido impide el agotamiento del socket.SocketsHttpHandlerrecorre las conexiones segúnPooledConnectionLifetimepara 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.
MyServicecrea una instancia de generador de clientes a partir del servicio, que se usa para crear un elementoHttpClient.HttpClientse utiliza para recuperar una página web.Maincrea un ámbito para ejecutar el métodoGetPagedel 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
HttpClientenStartup: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
HttpClientlógicas. Por ejemplo, se podría registrar un cliente 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 enHttpClient. - Administra la agrupación y la duración de las instancias de
HttpClientMessageHandlersubyacentes. La administración automática evita los problemas comunes de DNS (Sistema de nombres de dominio) que se producen al administrar la duración deHttpClientde 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
HttpClientde 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
HttpClientdeterminado 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
HttpClientse 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:
- Crea una instancia de
HttpClient. - Cree una instancia de
GitHubService, pasando la instancia deHttpClienta 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 que 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
TodoItemen JSON medianteSystem.Text.Json. Usa una instancia de JsonSerializerOptions para configurar el proceso de serialización. - Crea una instancia de StringContent a fin de empaquetar el JSON serializado para enviarlo en el cuerpo de la solicitud de HTTP.
- Llama a PostAsync para enviar el contenido de JSON a la URL especificada. Se trata de una URL relativa que se agrega a HttpClient.BaseAddress.
- Llama a EnsureSuccessStatusCode para iniciar una excepción si el código de estado de la respuesta no indica que la operación se ha procesado correctamente.
HttpClient también admite otros tipos de contenido. Por ejemplo, MultipartContent y StreamContent. Para obtener una lista completa del contenido admitido, vea HttpContent.
En el ejemplo siguiente se muestra una solicitud 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, vea 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 de salida. 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ívelo de DelegatingHandler.
- Reemplace SendAsync. Ejecute 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, considere 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.
Use uno de los siguientes enfoques para compartir el estado por solicitud con controladores de mensajes:
- Pase datos al controlador mediante HttpRequestMessage.Properties.
- Use IHttpContextAccessor para acceder a la solicitud actual.
- Cree un objeto de almacenamiento AsyncLocal<T> personalizado para pasar los datos.
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:
- HttpRequestException
- HTTP 5xx
- HTTP 408
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
AddTransientHttpErrorPolicyagrega 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:
- Se agregan las directivas "regular" y "long".
- AddPolicyHandlerFromRegistry agrega las directivas "regular" y "long" del registro.
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
HttpMessageHandlera intervalos regulares.
Existen formas alternativas de solucionar los problemas anteriores mediante una instancia de SocketsHttpHandler de larga duración.
- Cree una instancia de
SocketsHttpHandleral 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
HttpClientmediantenew 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.
SocketsHttpHandlercomparte las conexiones entre las instancias deHttpClient. Este uso compartido impide el agotamiento del socket.SocketsHttpHandlerrecorre las conexiones segúnPooledConnectionLifetimepara 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.
MyServicecrea una instancia de generador de clientes a partir del servicio, que se usa para crear un elementoHttpClient.HttpClientse utiliza para recuperar una página web.Maincrea un ámbito para ejecutar el métodoGetPagedel 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
HttpClientenStartup: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
HttpClientlógicas. Por ejemplo, se podría registrar un cliente 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 enHttpClient. - Administra la agrupación y la duración de las instancias de
HttpClientMessageHandlersubyacentes. La administración automática evita los problemas comunes de DNS (Sistema de nombres de dominio) que se producen al administrar la duración deHttpClientde 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
HttpClientde 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
HttpClientdeterminado 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
HttpClientse 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:
- Crea una instancia de
HttpClient. - Cree una instancia de
GitHubService, pasando la instancia deHttpClienta 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 que 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
TodoItemen JSON medianteSystem.Text.Json. Usa una instancia de JsonSerializerOptions para configurar el proceso de serialización. - Crea una instancia de StringContent a fin de empaquetar el JSON serializado para enviarlo en el cuerpo de la solicitud de HTTP.
- Llama a PostAsync para enviar el contenido de JSON a la URL especificada. Se trata de una URL relativa que se agrega a HttpClient.BaseAddress.
- Llama a EnsureSuccessStatusCode para iniciar una excepción si el código de estado de la respuesta no indica que la operación se ha procesado correctamente.
HttpClient también admite otros tipos de contenido. Por ejemplo, MultipartContent y StreamContent. Para obtener una lista completa del contenido admitido, vea HttpContent.
En el ejemplo siguiente se muestra una solicitud 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, vea 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 de salida. 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ívelo de DelegatingHandler.
- Reemplace SendAsync. Ejecute 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, considere 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.
Use uno de los siguientes enfoques para compartir el estado por solicitud con controladores de mensajes:
- Pase datos al controlador mediante HttpRequestMessage.Properties.
- Use IHttpContextAccessor para acceder a la solicitud actual.
- Cree un objeto de almacenamiento AsyncLocal<T> personalizado para pasar los datos.
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:
- HttpRequestException
- HTTP 5xx
- HTTP 408
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
AddTransientHttpErrorPolicyagrega 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:
- Se agregan las directivas "regular" y "long".
- AddPolicyHandlerFromRegistry agrega las directivas "regular" y "long" del registro.
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
HttpMessageHandlera intervalos regulares.
Existen formas alternativas de solucionar los problemas anteriores mediante una instancia de SocketsHttpHandler de larga duración.
- Cree una instancia de
SocketsHttpHandleral 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
HttpClientmediantenew 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.
SocketsHttpHandlercomparte las conexiones entre las instancias deHttpClient. Este uso compartido impide el agotamiento del socket.SocketsHttpHandlerrecorre las conexiones segúnPooledConnectionLifetimepara 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.
MyServicecrea una instancia de generador de clientes a partir del servicio, que se usa para crear un elementoHttpClient.HttpClientse utiliza para recuperar una página web.Maincrea un ámbito para ejecutar el métodoGetPagedel 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
HttpClientenStartup: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(...);