Índice
1 - Qué es HttpClient?
HttpClient
es la clase que nos proporciona C#
para hacer llamadas a través del protocolo HTTP
.
En el mundo laboral utilizamos HttpClient
cada vez que consultamos o enviamos información de una API.
Como funciona a través de HTTP el resultado (o response) nos indicará también el HttpStatusCode
el cual es muy útil para comprender el resultado obtenido.
Por ejemplo, los Status code de 200 a 299 son códigos que indican que todo ha ido bien. Siendo el 200 que todo está perfecto o 201 que lo que has intentado crear se ha creado con éxito.
Por ejemplo cuando realizamos una compra en una tienda, la API del back end devuelve al front end un 201 indicando que esa orden de compra se ha creado correctamente.
2 - Utilizar HttpClient correctamente
Para utilizar HttpClient debemos comprender primero la interfaz IDisposable, si no sabes lo que es, te dejo un enlace donde consultarlo.
Esto es debido a que HttpClient
implementa IDisposable
, por lo que debemos implementar nuestro método Dispose
o quizá no?
Antes de ver una vista más detallada o más en profundidad de cómo funciona HttpClient
, vamos a ver como funciona a rasgos generales.
Cabe indicar que como todo en C# podemos invocarlo desde cualquier parte de nuestro código pero yo, recomiendo incluirlo o bien en su propia librería o al menos en una estructura de carpetas dentro de nuestro código.
2.1 - Ejemplo HttpClient en C#
Como he indicado podemos construir nuestro HttpClient
desde cualquier lugar del código.
using (var client = new HttpClient())
{
//Aquí el código
}
Como sabemos del post de dispose en C# debemos poner nuestra instancia en el using para posteriormente utilizar el código, por ejemplo para leer una web directamente.
using (var client = new HttpClient())
{
var result = await client.GetAsync("https://www.netmentor.es");
Console.WriteLine(result.StatusCode);
}
2.1.1 - Ejemplo Get objeto utilizando HttClient
O si por ejemplo queremos leer un objeto directamente con net5
es posible; para este ejemplo voy a utlizar un proyecto en localhost que devuelve PerfilPersonalDto
en formato Json.
using (var client = new HttpClient())
{
var result = await client.GetFromJsonAsync<PersonalProfileDto>("https://localhost:44363/api/perfilpersonal/ivanabad");
Console.WriteLine(result.Website);
}
nota: podemos utilizar el mismo cliente para cualquiera de las acciones CRUD.
3 - Aplicar HttpClient Correctamente
Para este caso de uso y la explicación del problema de utilizar HttpClient dentro de un bloque using vamos a volver a nuestro caso anterior donde consultamos el código de respuesta de netmentor.
Pero esta vez, vamos a ponerlo en un bucle que lo ejecuta 10 veces.
static async Task Main(string[] args)
{
for(int i = 0; i<10; i++)
{
using var client = new HttpClient();
var result = await client.GetAsync("https://www.netmentor.es");
Console.WriteLine(result.StatusCode);
}
}
Como vemos si ejecutamos, la consola nos devuelve que todo está ok
.
Pero si corremos el comando netstat en la máquina servidor veremos que no está tan bien.
Si vemos el resultado es un poco diferente:
Como podemos ver un montón de conexiones siguen abiertas pero nuestra aplicación ha terminado. esto es porque la conexión ha sido cerrada en un lado (el código) pero el server sigue teniendo la conexión abierta por cierto tiempo por si hay algun delay o algo en en el proceso.
Aqui podemos ver un diagrama de como funciona el protocolo TCP/IP el cual lo he cogido de la siguente web -https://www4.cs.fau.de/Projects/JX/Projects/TCP/tcpstate.html -
Una posible solución a este problema es ir al servidor y reducir el tiempo que mantiene las conexiones abiertas, pero obviamente NO debemos hacerlo, primero, porque no siempre controlamos el servidor y segundo porque según google y diferentes foros es una muy mala práctica. (yo personalmente NUNCA lo he bajado).
Además tenemos un número máximo de sockets que podemos crear por lo que tampoco solucionaría mucho. (recordemos un socket por cada `using` que utilizamos)
3.1 - Aplicar la solución correcta
La solución en este caso es utilizar una única instancia de HttpClient
para la aplicación completa, así podemos reducir el número de sockets.
Lo que quiere decir que si vamos a utilizar inyeccion de dependencias debemos añadir la dependencia como singleton, aunque no es la mejor opción, ya que tendremos problemas con el DNS.
La solución mas común es incluirlo como static y finalmente no utilzes la funcionalidad de dispose. De esta forma lo que estamos haciendo es compartir el mismo socket
class Program
{
private static HttpClient Client = new HttpClient();
static async Task Main(string[] args)
{
for(int i = 0; i<10; i++)
{
var result = await Client.GetAsync("https://www.netmentor.es");
Console.WriteLine(result.StatusCode);
}
}
}
3.2 - Aplicar HttpClientFacotry
Microsoft, se dio cuenta del problema y de que la comunidad se quejaba constantemente. Por ello en la versión de .NET Core 2.1 introdujeron un nuevo tipo. HttpClientFactory
el cual nos hace la vida mucho más sencilla ya que trata por sí solo el tiempo de vida.
Para ello en nuestra clase startup.cs debemos incluir en los services services.AddHttpClient();
E inyectar la dependencia cuando vayamos a utilizarla, pero para utilizarla deberemos utilizar _httpClientFactory.CreateClient()
e indicar la BaseAddress
.
public class EjemploHttpClientFac
{
private readonly IHttpClientFactory _httpClientFactory;
public EjemploHttpClientFac(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task Test()
{
var client = _httpClientFactory.CreateClient();
client.BaseAddress = new Uri("https://localhost:44363/");
var result =await client.GetAsync("api/perfilpersonal/ivanabad");
Console.WriteLine(result.StatusCode);
}
}
obviamente tener que configurar la base address en cada una de nuestras, es mucho mas fácil configurarlo una vez y tirar siempre del mismo, para ello podemos configurar uno o varios clientes, definiendo un “id” dentro del AddHttpClient()
del startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient("BackEnd", client =>
{
client.BaseAddress = new Uri("https://localhost:44363/");
});
}
Únicamente debemos indicar que “key” vamos a utilizar cuando hacemos “CreateClient()
”
public async Task Test()
{
var client = _httpClientFactory.CreateClient("BackEnd");
var result =await client.GetAsync("api/perfilpersonal/ivanabad");
Console.WriteLine(result.StatusCode);
}
y el resultado es el mismo.
Conclusión
El uso de HttpClient para mi cambio desde que descubrí este bug, ya que es muy difícil de detectar, de hecho, es posible que no lo veas fallar nunca, pero donde yo lo descubrí fue en un pico de uso de uno de nuestros servicios serverless, básicamente nos quedamos sin sockets.
Durante toda mi vida he hecho el dispose en todo objeto que implementa IDisposable, lo que no entiendo es porque el propio lenguaje te indica que lo estás haciendo mal, ya bien sea en tiempo de compilación o en tiempo de ejecución, ya que si haces siempre dispose HttpClient lo único que puede pasar es que tu aplicación no funcione como esperabas.
Y desde luego la solución más sencilla si estamos en código que va más allá de net core 2.1 es utilizar HttpClientFactory.