HTTP con .NET
En este artículo, aprenderá a usar los tipos IHttpClientFactory y HttpClient con diversos aspectos básicos de .NET, como la inserción de dependencias (ID), el registro y la configuración. El tipo HttpClient se introdujo en .NET Framework 4.5, que se publicó en 2012. En otras palabras, ha estado en uso desde hace un tiempo. HttpClient se usa para hacer solicitudes HTTP y controlar las respuestas HTTP de los recursos web identificados por un Uri. El protocolo HTTP compone la mayor parte de todo el tráfico de Internet.
Dado que los principios modernos de desarrollo de aplicaciones rigen los procedimientos recomendados, IHttpClientFactory actúa como una abstracción de fábrica que puede crear instancias de HttpClient con configuraciones personalizadas. IHttpClientFactory se presentó en .NET Core 2.1. Las cargas de trabajo de .NET comunes basadas en HTTP pueden aprovechar con facilidad el middleware de terceros resistente y con control de errores transitorios.
Exploración del tipo IHttpClientFactory
Todo el código fuente de ejemplo de este artículo se basa en el paquete NuGet Microsoft.Extensions.Http. Además, la API gratuita The Internet Chuck Norris Database se usa para hacer solicitudes HTTP GET para bromas "de nerds".
Cuando llama a cualquiera de los métodos de extensión AddHttpClient, está agregando IHttpClientFactory y los servicios y relacionados a IServiceCollection. El tipo IHttpClientFactory ofrece las ventajas siguientes:
- Expone la clase
HttpClientcomo tipo listo para la inserción de dependencias. - Proporciona una ubicación central para denominar y configurar instancias de
HttpClientlógicas. - Codifica el concepto de middleware de salida a través de la delegación de controladores en
HttpClient. - Proporciona métodos de extensión para el middleware basado en Polly a fin de aprovechar los controladores de delegación en
HttpClient. - Administra la agrupación y la duración de las instancias de HttpClientHandler subyacentes. La administración automática evita los problemas comunes del Sistema de nombres de dominio (DNS) que se producen al administrar la duración de
HttpClientde forma manual. - Agrega una experiencia de registro configurable (a través de ILogger) en todas las solicitudes enviadas a través de los clientes creados por Factory.
Patrones de consumo
IHttpClientFactory se puede usar de varias formas en una aplicación:
El mejor enfoque depende de los requisitos de la aplicación.
Uso básico
Para registrar IHttpClientFactory, llame a AddHttpClient:
using BasicHttp.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddHttpClient();
services.AddTransient<JokeService>();
})
.Build();
El consumo de servicios puede requerir IHttpClientFactory como parámetro de constructor con la inserción de dependencias. En el código siguiente se usa IHttpClientFactory para crear una instancia de HttpClient:
using System.Net.Http.Json;
using Microsoft.Extensions.Logging;
using Shared;
namespace BasicHttp.Example;
public class JokeService
{
private readonly IHttpClientFactory _httpClientFactory = null!;
private readonly ILogger<JokeService> _logger = null!;
public JokeService(
IHttpClientFactory httpClientFactory,
ILogger<JokeService> logger) =>
(_httpClientFactory, _logger) = (httpClientFactory, logger);
public async Task<string> GetRandomJokeAsync()
{
// Create the client
HttpClient client = _httpClientFactory.CreateClient();
try
{
// Make HTTP GET request
// Parse JSON response deserialize into ChuckNorrisJoke type
ChuckNorrisJoke? result = await client.GetFromJsonAsync<ChuckNorrisJoke>(
"https://api.icndb.com/jokes/random?limitTo=[nerdy]",
DefaultJsonSerialization.Options);
if (result?.Value?.Joke is not null)
{
return result.Value.Joke;
}
}
catch (Exception ex)
{
_logger.LogError("Error getting something fun to say: {Error}", ex);
}
return "Oops, something has gone wrong - that's not funny at all!";
}
}
El uso de IHttpClientFactory como en el ejemplo anterior es una buena manera de refactorizar una aplicación existente. No tiene efecto alguno en la forma de usar HttpClient. En aquellos sitios de una aplicación existente en los que ya se hayan creado instancias de HttpClient, reemplace esas repeticiones por llamadas a CreateClient.
Clientes con nombre
Los clientes con nombre son una buena opción cuando:
- La aplicación requiere muchos usos distintos de
HttpClient. - Muchas instancias de
HttpClienttienen otra configuración.
La configuración de un objeto HttpClient con nombre se puede realizar durante la fase de registro en ConfigureServices:
using NamedHttp.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices((context, services) =>
{
string httpClientName = context.Configuration["JokeHttpClientName"];
services.AddHttpClient(
httpClientName,
client =>
{
// Set the base address of the named client.
client.BaseAddress = new Uri("https://api.icndb.com/");
// Add a user-agent default request header.
client.DefaultRequestHeaders.UserAgent.ParseAdd("dotnet-docs");
});
services.AddTransient<JokeService>();
})
.Build();
En el código anterior, el cliente está configurado con:
- Un nombre que se extrae de la configuración en
"JokeHttpClientName". - La dirección base.
https://api.icndb.com/. - Encabezado de
"User-Agent".
Puede usar la configuración para especificar nombres de cliente HTTP, lo que resulta útil para evitar errores de nomenclatura de clientes al agregar y crear. En este ejemplo, el archivo appsettings.json se usa para configurar el nombre de cliente HTTP:
{
"JokeHttpClientName": "ChuckNorrisJokeApi"
}
Es fácil ampliar esta configuración y almacenar más detalles sobre cómo quiere que funcione el cliente HTTP. Para obtener más información, vea Configuración en .NET.
Crear el cliente
Cada vez que se llama a CreateClient:
- Se crea una instancia de
HttpClient. - Se llama a la acción de configuración.
Para crear un cliente con nombre, pase su nombre a CreateClient:
using System.Net.Http.Json;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Shared;
namespace NamedHttp.Example;
public class JokeService
{
private readonly IHttpClientFactory _httpClientFactory = null!;
private readonly IConfiguration _configuration = null!;
private readonly ILogger<JokeService> _logger = null!;
public JokeService(
IHttpClientFactory httpClientFactory,
IConfiguration configuration,
ILogger<JokeService> logger) =>
(_httpClientFactory, _configuration, _logger) =
(httpClientFactory, configuration, logger);
public async Task<string> GetRandomJokeAsync()
{
// Create the client
string httpClientName = _configuration["JokeHttpClientName"];
HttpClient client = _httpClientFactory.CreateClient(httpClientName);
try
{
// Make HTTP GET request
// Parse JSON response deserialize into ChuckNorrisJoke type
ChuckNorrisJoke? result = await client.GetFromJsonAsync<ChuckNorrisJoke>(
"jokes/random?limitTo=[nerdy]",
DefaultJsonSerialization.Options);
if (result?.Value?.Joke is not null)
{
return result.Value.Joke;
}
}
catch (Exception ex)
{
_logger.LogError("Error getting something fun to say: {Error}", ex);
}
return "Oops, something has gone wrong - that's not funny at all!";
}
}
En el código anterior, no es necesario especificar un nombre de host en la solicitud HTTP. El código puede pasar solo la ruta de acceso, ya que se usa la dirección base configurada del cliente.
Clientes con tipo
Clientes con tipo:
- Proporcionan las mismas funciones que los clientes con nombre sin la necesidad de usar cadenas como claves.
- Ofrece ayuda relativa al compilador e IntelliSense al consumir clientes.
- Facilitan una sola ubicación para configurar un elemento
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:
using System.Net.Http.Json;
using Microsoft.Extensions.Logging;
using Shared;
namespace TypedHttp.Example;
public sealed class JokeService
{
private readonly HttpClient _httpClient = null!;
private readonly ILogger<JokeService> _logger = null!;
public JokeService(
HttpClient httpClient,
ILogger<JokeService> logger) =>
(_httpClient, _logger) = (httpClient, logger);
public async Task<string> GetRandomJokeAsync()
{
try
{
// Make HTTP GET request
// Parse JSON response deserialize into ChuckNorrisJoke type
ChuckNorrisJoke? result = await _httpClient.GetFromJsonAsync<ChuckNorrisJoke>(
"https://api.icndb.com/jokes/random?limitTo=[nerdy]",
DefaultJsonSerialization.Options);
if (result?.Value?.Joke is not null)
{
return result.Value.Joke;
}
}
catch (Exception ex)
{
_logger.LogError("Error getting something fun to say: {Error}", ex);
}
return "Oops, something has gone wrong - that's not funny at all!";
}
}
En el código anterior:
- La configuración se establece cuando el cliente con tipo se agrega a la colección de servicios.
HttpClientse asigna como variable con ámbito de clase (campo) y se usa con las API expuestas.
Se pueden crear métodos específicos de la API que exponen la funcionalidad de HttpClient. Por ejemplo, el método GetRandomJokeAsync encapsula el código para recuperar una broma aleatoria.
En el código siguiente se llama a AddHttpClient en ConfigureServices para registrar una clase de cliente con tipo:
using TypedHttp.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddHttpClient<JokeService>(
client =>
{
// Set the base address of the named client.
client.BaseAddress = new Uri("https://api.icndb.com/");
// Add a user-agent default request header.
client.DefaultRequestHeaders.UserAgent.ParseAdd("dotnet-docs");
});
services.AddTransient<JokeService>();
})
.Build();
El cliente con tipo se registra como transitorio con inserción con dependencias, En el código anterior, AddHttpClient registra JokeService como servicio transitorio. Este registro usa un Factory Method para:
- Crea una instancia de
HttpClient. - Cree una instancia de
JokeService, pasando la instancia deHttpClienta su constructor.
Sugerencia
Una llamada a AddHttpClient<TClient> no agrega el servicio TClient aIServiceCollection. Aún tiene que agregarlo explícitamente con Add{ServiceLifetime}.
Clientes generados
IHttpClientFactory se puede usar en combinación con bibliotecas de terceros, como Refit. Refit es una biblioteca de REST para .NET Permite definiciones de API REST declarativas, con lo que se asignan métodos de interfaz a puntos de conexión. Se genera una implementación de la interfaz dinámicamente por medio de RestService, usando HttpClient para realizar las llamadas HTTP externas.
Considere los tipos record siguientes:
namespace Shared;
public record IdentifiableJokeValue(
int Id, string Joke);
namespace Shared;
public record ChuckNorrisJoke(
string Type,
IdentifiableJokeValue Value);
El ejemplo siguiente se basa en el paquete NuGet Refit.HttpClientFactory y es una interfaz sencilla:
using Refit;
using Shared;
namespace GeneratedHttp.Example;
public interface IJokeService
{
[Get("/jokes/random?limitTo=[nerdy]")]
Task<ChuckNorrisJoke> GetRandomJokeAsync();
}
La interfaz de C# anterior:
- Define un método denominado
GetRandomJokeAsyncque devuelve una instancia deTask<ChuckNorrisJoke>. - Declara un atributo
Refit.GetAttributecon la ruta de acceso y la cadena de consulta a la API externa.
Un cliente con tipo se puede agregar usando Refit para generar la implementación:
using GeneratedHttp.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Refit;
using Shared;
using IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddRefitClient<IJokeService>()
.ConfigureHttpClient(client =>
{
// Set the base address of the named client.
client.BaseAddress = new Uri("https://api.icndb.com/");
// Add a user-agent default request header.
client.DefaultRequestHeaders.UserAgent.ParseAdd("dotnet-docs");
});
})
.Build();
La interfaz definida se puede consumir cuando sea preciso, con la implementación proporcionada por la inserción de dependencias y Refit.
Realización de solicitudes POST, PUT y DELETE
En los ejemplos anteriores, todas las solicitudes HTTP usan el verbo 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(Item item)
{
using StringContent json = new(
JsonSerializer.Serialize(item, DefaultJsonSerialization.Options),
Encoding.UTF8,
MediaTypeNames.Application.Json);
using HttpResponseMessage httpResponse =
await _httpClient.PostAsync("/api/items", json);
httpResponse.EnsureSuccessStatusCode();
}
En el código anterior, el método CreateItemAsync:
- Serializa el parámetro
Itemen 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 UpdateItemAsync(Item item)
{
using StringContent json = new(
JsonSerializer.Serialize(item, DefaultJsonSerialization.Options),
Encoding.UTF8,
MediaTypeNames.Application.Json);
using HttpResponseMessage httpResponse =
await _httpClient.PutAsync($"/api/items/{item.Id}", json);
httpResponse.EnsureSuccessStatusCode();
}
El código anterior es muy similar al ejemplo de POST. El método UpdateItemAsync llama a PutAsync en lugar de PostAsync.
En el ejemplo siguiente se muestra una solicitud DELETE HTTP:
public async Task DeleteItemAsync(Guid id)
{
using HttpResponseMessage httpResponse =
await _httpClient.DeleteAsync($"/api/items/{id}");
httpResponse.EnsureSuccessStatusCode();
}
En el código anterior, el método DeleteItemAsync llama a DeleteAsync. Debido a que las solicitudes DELETE HTTP normalmente no contienen ningún cuerpo, el método DeleteAsync no proporciona ninguna sobrecarga que acepte una instancia de HttpContent.
Para obtener más información sobre el uso de verbos HTTP diferentes con HttpClient, vea HttpClient.
Administración de la duración de HttpClient
Cada vez que se llama a CreateClient en IHttpClientFactory, se devuelve una nueva instancia de HttpClient. Se crea una instancia de HttpClientHandler por cliente. La fábrica administra la duración de las instancias de HttpClientHandler.
IHttpClientFactory agrupa las instancias de HttpClientHandler creadas por Factory para reducir el consumo de recursos. Se puede reutilizar una instancia de HttpClientHandler 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 su propia conexión HTTP subyacente. Crear más controladores de los necesarios puede provocar retrasos en la conexión. Además, algunos controladores dejan las conexiones abiertas de forma indefinida, lo que puede ser un obstáculo a la hora de reaccionar ante los cambios de DNS.
La duración de controlador predeterminada es dos minutos. Para invalidar el valor predeterminado, llame a SetHandlerLifetime para cada cliente, en IServiceCollection:
services.AddHttpClient("Named.Client")
.SetHandlerLifetime(TimeSpan.FromMinutes(5));
Importante
Por lo general, puede tratar las instancias de HttpClient como objetos 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.
Configuración de HttpMessageHandler
Puede que sea necesario controlar la configuración del elemento HttpMessageHandler interno usado por un cliente.
Se devuelve un IHttpClientBuilder cuando se agregan clientes con nombre o con tipo. Se puede usar el método de extensión ConfigurePrimaryHttpMessageHandler para definir un delegado en IServiceCollection. Este delegado servirá para crear y configurar el elemento principal HttpMessageHandler que ese cliente usa:
services.AddHttpClient("Named.Client")
.ConfigurePrimaryHttpMessageHandler(() =>
{
return new HttpClientHandler
{
AllowAutoRedirect = false,
UseDefaultCredentials = true
};
});
Configuración adicional
Hay varias opciones de configuración adicionales para controlar IHttpClientHandler:
| Método | Descripción |
|---|---|
| AddHttpMessageHandler | Agrega un controlador de mensajes adicional para una clase HttpClient con nombre. |
| AddTypedClient | Configura el enlace entre el tipo TClient y la clase HttpClient con nombre asociada a la interfaz IHttpClientBuilder. |
| ConfigureHttpClient | Agrega un delegado que se usará para configurar un objeto HttpClient con nombre. |
| ConfigureHttpMessageHandlerBuilder | Agrega un delegado que se usará para configurar controladores de mensajes mediante HttpMessageHandlerBuilder para un objeto HttpClient con nombre. |
| ConfigurePrimaryHttpMessageHandler | Configura la clase HttpMessageHandler principal del contenedor de inserción de dependencias para una clase HttpClient con nombre. |
| RedactLoggedHeaders | Establece la colección de nombres de encabezado HTTP para los que se deben censurar los valores antes del registro. |
| SetHandlerLifetime | Establece el período de tiempo que se puede volver a usar una instancia de HttpMessageHandler. Cada cliente con nombre puede tener configurado su propio valor de duración de controlador. |