En muchos casos escuchamos sobre Cache. El almacenamiento en caché puede mejorar el rendimiento y la escalabilidad de las aplicaciones al reducir el trabajo en procesamiento y reducir en muchos casos el tráfico de red. Existen grandes herramientas de caché como Redis que nos ayudan a que nuestras aplicaciones tengan respuestas más rápidas. En este post no veremos Redis, veremos las opciones que tenemos dentro del framework de .Net para obtener esta funcionalidad.
Caché en Memoria
EN. Net tenemos 2 tipos de almacenamiento en caché disponibles en nuestras aplicaciones. El primero es el caché de almacenamiento de salida y nos permite almacenar dinámicamente controles y páginas en todo dispositivo que soporte HTTP 1.1. Luego en solicitudes posteriores, no se ejecutara todo el código nuevamente si no que será retornado el resultado almacenado en el caché.
El segundo tipo disponible es el almacenamiento de datos en caché. Por ejemplo, si consultamos una base de datos y obtenemos registros los almacenaremos en memoria. En la siguiente consulta, consultaremos si tenemos datos disponibles en caché para ello, si es así, se retornará, si no es así se volverán a consultar a la base de datos y serán almacenados en caché para futuras consultas.
En la memoria caché podemos almacenar cualquier tipo de objeto o colecciones de objetos que nos ayudarán a aumentar el rendimiento de nuestras aplicaciones. Tengamos presente que no es lo ideal para aplicaciones distribuidas ya que mantendrá el cache en la instancia que se está invocando, y si tenemos múltiples servicios balanceados, el cache no estará presente al mismo tiempo en todos.
Manos a la obra
Lo primero que haremos es crear un proyecto y agregar el desde el repositorio Nuget a la librería Microsoft.Extensions.Caching.Memory de la siguiente manera.
dotnet new webapp dotnet add package Microsoft.Extensions.Caching.Memory
Una vez referido el paquete, para tener disponible el servicio de caché deberemos agregar el servicio por medio de inyección de dependencia. Para esto utilizaremos la interfaz IMemoryCache que será pasado por el constructor a los que deseen utilizarlos. Veamos se vería en una clase genérica:
using Microsoft.Extensions.Caching.Memory; public class GenericClass { private readonly IMemoryCache _memoryCache; public GenericClass(IMemoryCache memoryCache) => _memoryCache = memoryCache; ...
Para inyectar debemos ir a nuestro program.cs y agrega la linea:
builder.Services.AddMemoryCache();
Ahora veamos un código de caché implementado en alto nivel.
public async Task<List<Customer>> GetCustomersCacheAsync() { var output = _memoryCache.Get<List<Customer>>("customers"); if (output is not null) return output; output = new() { new() { FirstName = "Homer", LastName = "Simpsons" }, new() { FirstName = "Moe", LastName = "Sislak" }, new() { FirstName = "Barny", LastName = "Gomez" } }; _memoryCache.Set("customers", output, TimeSpan.FromMinutes(1)); return output; }
Veamos paso a paso lo que hace nuestro método. La primera vez que se ejecuta intentara tomar del cache la lista de Customer (línea 3). Luego validará si no es null, como es la primera vez lo estará y seguirá para construir el objeto con los clientes, en nuestro caso el output. Aquí viene nuestra línea clave:
_memoryCache.Set("users", output, TimeSpan.FromMinutes(1));
En esta línea seteamos nuestro objeto en caché. Posee 3 partes, primero la key con la que reconoceremos a nuestro cache, luego lo que deseamos guardar, por último el tiempo que daemon guardarlo en memoria. Pasado este tiempo el caché se eliminará. Entonces, en una segunda invocación, al preguntar si es null, al no serlo, retorna directamente el output que almacenamos en la primera petición.
En un gráfico para ser más claros:
Existe un camino más eficiente para validar si un caché existe:
public async Task<List<Customer>> GetCustomersCacheAsync() { List<Customer> customers = null; if (_memoryCache.TryGetValue("customers", out customers)) { return customers; } output = new() { new() { FirstName = "Homer", LastName = "Simpsons" }, new() { FirstName = "Moe", LastName = "Sislak" }, new() { FirstName = "Barny", LastName = "Gomez" } }; _memoryCache.Set("customers", output, TimeSpan.FromMinutes(1)); return output; }
Hablemos un poco del tiempo de expiración: Sliding Expiration y Absolute Expiration. Sliding Expiration cadura el caché si no es accedido durante el tiempo establecido. Absolute Expiration el cache caducará en el tiempo establecido sin importar si se consume o no.
Para poder configurar alguno de estos 2 tipos usaremos el objeto MemoryCacheEntryOptions que nos permitirá configurar estas opciones veamos.
Sliding Expiration
// Configurar cache var cacheOptions = new MemoryCacheEntryOptions() .SetSlidingExpiration(TimeSpan.FromSeconds(30));
Absolute Expiration
// Configurar cache var cacheOptions = new MemoryCacheEntryOptions() .SetAbsoluteExpiration(TimeSpan.FromSeconds(30));
por último:
// Setear el objeto _memoryCache.Set("customers", output, cacheOptions)
¿Puedo guardar todo lo que quiera en la memoria caché? Si, pero tenemos que tener presente que la memoria de nuestros servidores son limitadas. Si llenamos la memoria podremos tener algunos inconvenientes en las respuestas de nuestra aplicación. Por esto es posible establecer un límite de uso de memoria para no crecer sin límite.
{ public MemoryCache Cache { get; } = new MemoryCache( new MemoryCacheOptions { SizeLimit = 1024 }); }
Conclusiones
El uso de cache en memoria puede sernos muy útiles para aumentar el rendimiento de nuestra aplicación y reducir el tráfico de red hacia los repositorios de datos. Pero es un recurso que debemos utilizar con cautela por no ocupar toda nuestra memoria y también tener presente el momento correcto de la expiración del caché. Espero que te haya gustado el post. Déjame tus comentarios.