Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 4 de julio de 2023
.NET

Seguro que habéis visto más de una vez un código parecido al siguiente, en el que llamamos a una API  REST externa y su resultado es deserializado a un objeto .NET para introducirlo en el flujo de la aplicación:

async Task<User[]> GetUsersAsync()
{
    var httpClient = _httpClientFactory.CreateClient();
    
    // Hacemos la llamada
    var response = await httpClient.GetAsync("https://jsonplaceholder.typicode.com/users");

    // Si la cosa no fue bien, retornamos
    if (!response.IsSuccessStatusCode)
        return Array.Empty<User>();

    // Descargamos la respuesta y la deserializamos
    var usersAsJson = await response.Content.ReadAsStringAsync();
    var users = JsonSerializer.Deserialize<User[]>(usersAsJson);

    return users;
}

Fijaos que el JSON de la respuesta de la API lo guardamos en una cadena de caracteres para, justo después, deserializarlo y convertirlo en un array de objetos User. Que levante la mano el que no lo haya hecho nunca 😉

¿Y veis dónde está el problema? A la salida de este método, tendremos en memoria dos copias de los datos de los usuarios, una en forma de string JSON y otra en el objeto que hemos deserializado.

Si estamos hablando de respuestas pequeñas o con poca concurrencia, probablemente el impacto es inapreciable. Pero si las estructuras retornadas por la API tuvieran un tamaño considerable o estamos en un escenario de múltiples llamadas simultáneas, esta duplicidad sería un auténtico derroche de recursos.

Una forma más correcta de hacer esto mismo es la que se muestra justo debajo. Como podéis observar, en este caso no estamos descargando la respuesta como un string, sino que la estamos deserializando directamente desde el stream por el que se están descargando los datos, con lo que se reducirá el uso de la memoria:

public async Task<User[]> GetUsersAsync()
{
    var httpClient = _httpClientFactory.CreateClient();

    // Hacemos la llamada
    var response = await httpClient.GetAsync("https://jsonplaceholder.typicode.com/users");

    // Si la cosa no fue bien, retornamos
    if (!response.IsSuccessStatusCode)
        return Array.Empty<User>();

    // Deserializamos directamente la respuesta
    var users = await response.Content.ReadFromJsonAsync<User[]>();
    return users;
}

¿Y qué incidencia tiene esto en la práctica?

Aunque de forma intuitiva ya se ve que esta segunda opción debería ser más eficiente que la primera, vamos a comprobarlo usando el método científico, así que, como otras veces, vamos a recurrir al amigo BenchmarkDotNet para obtener algunas métricas que lo demuestren.

Realizando una prueba de rendimiento a las dos opciones, podemos ver que tardan un tiempo muy parecido en ejecutarse, aunque hay un pequeño porcentaje de ventaja para la opción de descargar primero en el string y luego deserializar desde éste. Sin embargo, el impacto de esta opción en los allocations es de más del doble respecto a la deserialización directa desde el stream. Es decir, deserializar desde el stream ahorrará una buena cantidad de memoria a costa de una pequeña pérdida de rendimiento.

|      Method |     Mean |   Error |  StdDev |     Gen0 |     Gen1 |    Gen2 | Allocated |
|------------ |---------:|--------:|--------:|---------:|---------:|--------:|----------:|
| UsingStream | 49.75 ms | 0.67 ms | 0.59 ms | 1272.727 | 1181.818 | 363.636 |  10.39 MB |
| UsingString | 48.10 ms | 0.88 ms | 0.78 ms | 1272.727 | 1181.818 | 363.636 |  25.34 MB |

Publicado en Variable not found.

Aún no hay comentarios, ¡sé el primero!