Sobre HybridCache y cómo sacar más partido a DotNet
Recientemente se ha lanzado .NET 9, que no es una versión de soporte extendido (LTS). Ya compartí mi opinión sobre el tema del versionado y cómo lo gestionamos en ocasiones en el artículo que escribí para CompartiMOSS hace un tiempo. Si me preguntáis si en los proyectos en los que trabajamos vamos a migrar a .NET 9, mi respuesta es un rotundo SÍ.
Aunque muchos saben que dentro de un año llegará .NET 10, una versión con soporte a largo plazo, el soporte adicional que ofrece no siempre justifica el esfuerzo de quedarse en una versión LTS durante más tiempo. En el tiempo que ahorras al evitar actualizar de versión en versión, acabas perdiendo rendimiento y los beneficios de las nuevas características.
Uno de los grandes avances en .NET 9 es la optimización en el uso de memoria. En nuestro caso, al desplegar productos en AKS, el tamaño de los pods en términos de CPU y memoria es un factor crítico. Históricamente, los procesos en .NET tenían un consumo de memoria que crecía de forma constante hasta que se reiniciaban. Sin embargo, gracias a las mejoras en el Garbage Collector, este comportamiento se ha corregido notablemente.
Introducción
Sin embargo, hoy no vengo a profundizar en ese tema (lo dejaré para otro artículo), sino a reflexionar sobre otra novedad: HybridCache. Resulta curioso que esta funcionalidad destaque tanto dentro de la comunidad, especialmente cuando es algo que ya venimos implementando desde .NET 3. Para nosotros, no es un argumento suficientemente fuerte para justificar la actualización de versión, lo cual me lleva a preguntarme si los desarrolladores realmente conocen las entrañas de .NET.
¿Qué es el patrón Cache?
El patrón Cache es una técnica de diseño que optimiza el acceso a recursos costosos de calcular o recuperar, almacenándolos temporalmente en una memoria rápida. Esto mejora el rendimiento y reduce la latencia de las aplicaciones. Sus principales beneficios son:
- Reducir la latencia: Evitar operaciones repetitivas como consultas a bases de datos o llamadas a APIs.
- Mejorar el rendimiento: Proporcionar acceso rápido a datos previamente procesados o recuperados.
- Optimizar recursos: Reducir el uso innecesario de CPU, ancho de banda o almacenamiento.
Implementación en .NET
.NET ofrece varias opciones para implementar una caché. Por ejemplo, con IMemoryCache, los datos se almacenan directamente en la memoria de la aplicación. En cambio, al usar la interfaz IDistributedCache
, los datos se guardan en un sistema externo como Redis o SQL. Podéis explorar más sobre estas opciones en la documentación oficial.
CacheService: Una solución personalizada
En nuestros desarrollos, utilizamos una librería propia para implementar una arquitectura de datos poliglota que maximice la eficiencia económica y técnica. Basándonos en una implementación previa de Fernando Escolar y Jorge Turrado, surgió CacheService, una solución Open Source compatible desde .NET 7.
Configuración de CacheService
Para instalar CacheService, basta con ejecutar:
dotnet add package CacheService
Asegúrate de que las dependencias necesarias estén registradas en tu aplicación:
services.AddLogging();
services.AddMemoryCache();
services.AddStackExchangeRedisCache(op => ...);
services.AddCacheService();
Una vez configurado, puedes integrarlo en una Minimal API:
public record struct Request(
[FromServices] AvengerDbContext Database,
[FromServices] ICacheService Cache,
[FromQuery] int Page = 1,
[FromQuery] int PageSize = 10
);
protected override async Task<IResult> HandleAsync(Request req, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var total = await req.Database.Avengers.CountAsync(cancellationToken);
if (total == 0) return Results.NoContent();
var avengers = await req.Cache.GetOrSetAsync(
$"avengers",
async token => await req.Database.Avengers
.Select(b => new ResponseItem
{
Id = b.Id,
Name = b.Name,
Photo = b.Photo,
Strength = b.Strength
})
.OrderBy(b => b.Name)
.Skip((req.Page - 1) * req.PageSize)
.Take(req.PageSize)
.ToListAsync(cancellationToken),
cancellationToken: cancellationToken
);
return Results.Ok(new Response(avengers, total, req.Page, req.PageSize));
}
HybridCache: ¿Qué aporta?
HybridCache se instala mediante:
dotnet add package Microsoft.Extensions.Caching.Hybrid
Proporciona opciones avanzadas como límites de tamaño de los elementos en caché y configuraciones específicas de expiración:
builder.Services.AddHybridCache(options =>
{
options.MaximumPayloadBytes = 10 * 1024 * 1024; // 10 MB
options.MaximumKeyLength = 512;
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(30),
LocalCacheExpiration = TimeSpan.FromMinutes(30)
};
});
Ejemplo de uso con HybridCache
Al igual que con CacheService, puedes integrarlo fácilmente:
public record struct Request(
[FromServices] AvengerDbContext Database,
[FromServices] Microsoft.Extensions.Caching.Hybrid.HybridCache Cache,
[FromQuery] int Page = 1,
[FromQuery] int PageSize = 10
);
protected override async Task<IResult> HandleAsync(Request req, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var total = await req.Database.Avengers.CountAsync(cancellationToken);
if (total == 0) return Results.NoContent();
var avengers = await req.Cache.GetOrCreateAsync(
$"avengers",
async token => await req.Database.Avengers
.Select(b => new ResponseItem
{
Id = b.Id,
Name = b.Name,
Photo = b.Photo,
Strength = b.Strength
})
.OrderBy(b => b.Name)
.Skip((req.Page - 1) * req.PageSize)
.Take(req.PageSize)
.ToListAsync(cancellationToken),
cancellationToken: cancellationToken
);
return Results.Ok(new Response(avengers, total, req.Page, req.PageSize));
}
Resumen
Este artículo no busca explicar en detalle el funcionamiento de HybridCache, sino fomentar el pensamiento crítico sobre nuestras elecciones tecnológicas. Aunque HybridCache puede ser útil, en nuestro caso no aporta beneficios adicionales frente a CacheService.
¿Vosotros qué opináis? ¡Os leo!
Happy coding