Biblioteca de memoria caché avanzada para .NET 8+ con funcionalidades extendidas de almacenamiento en memoria, gestión automática de expiración, políticas de limpieza configurables y soporte para serialización. Incluye decoradores para logging, métricas de rendimiento y patrones de cache-aside con invalidación inteligente.
$ dotnet add package EV.MemoryCacheLibrería de Cache en Memoria de Alto Rendimiento: Una librería simple y eficiente para .NET 8 que gestiona el cache en memoria usando IMemoryCache con opciones de configuración avanzadas y logging comprehensivo.
dotnet add package EV.MemoryCache
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"EV.MemoryCache": "Debug"
}
},
"AllowedHosts": "*",
"MemoryCacheConfiguration": {
"Enabled": true,
"DefaultExpirationMinutes": 30,
"DefaultSlidingExpirationMinutes": 5,
"SizeLimit": 3000,
"CompactionPercentage": 0.25
}
}
using EV.MemoryCache;
var builder = WebApplication.CreateBuilder(args);
// Agregar servicios al contenedor
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Registrar el servicio de EV.MemoryCache
builder.Services.AddMemoryCacheService(builder.Configuration);
var app = builder.Build();
// Configurar el pipeline
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
using EV.MemoryCache.Interfaces;
public class ProductService
{
private readonly IMemoryCacheManager _cache;
private readonly IProductRepository _repository;
public ProductService(IMemoryCacheManager cache, IProductRepository repository)
{
_cache = cache;
_repository = repository;
}
public async Task<Product?> GetProductAsync(int productId)
{
var cacheKey = $"product:{productId}";
// Usar GetOrSet para cache automático
return _cache.GetOrSet(cacheKey, () =>
{
// Esto solo se ejecuta si no está en cache
return _repository.GetById(productId);
}, TimeSpan.FromMinutes(15));
}
public void UpdateProduct(Product product)
{
_repository.Update(product);
// Actualizar cache con datos frescos
var cacheKey = $"product:{product.Id}";
_cache.Set(cacheKey, product);
}
public void DeleteProduct(int productId)
{
_repository.Delete(productId);
// Remover del cache
_cache.Remove($"product:{productId}");
}
}
| Propiedad | Tipo | Por Defecto | Descripción |
|---|---|---|---|
Enabled | bool | true | Habilitar/deshabilitar cache globalmente |
DefaultExpirationMinutes | int | 30 | Tiempo de expiración absoluto por defecto |
DefaultSlidingExpirationMinutes | int? | null | Tiempo de expiración deslizante por defecto |
SizeLimit | long? | null | Número máximo de entradas en cache |
CompactionPercentage | double? | null | Porcentaje a remover cuando se alcanza el límite (0.0-1.0) |
{
"MemoryCacheConfiguration": {
"Enabled": true,
"DefaultExpirationMinutes": 5,
"DefaultSlidingExpirationMinutes": 2,
"SizeLimit": 100
}
}
{
"MemoryCacheConfiguration": {
"Enabled": true,
"DefaultExpirationMinutes": 60,
"DefaultSlidingExpirationMinutes": 15,
"SizeLimit": 50000,
"CompactionPercentage": 0.20
}
}
{
"MemoryCacheConfiguration": {
"Enabled": false
}
}
| Método | Descripción | Parámetros |
|---|---|---|
Get<T>(key) | Obtiene un elemento del cache | string key |
Set<T>(key, value, expiration?) | Almacena un elemento en cache | string key, T value, TimeSpan? expiration |
GetOrSet<T>(key, factory, expiration?) | Obtiene del cache o crea usando factory | string key, Func<T> factory, TimeSpan? expiration |
Exists(key) | Verifica si existe una clave en cache | string key |
Remove(key) | Remueve un elemento del cache | string key |
// Almacenar en cache
_cache.Set("user:123", userObject, TimeSpan.FromMinutes(30));
// Obtener del cache
var user = _cache.Get<User>("user:123");
// Verificar si existe
if (_cache.Exists("user:123"))
{
// La clave existe en cache
}
// Remover del cache
_cache.Remove("user:123");
// Patrón más eficiente - maneja cache miss automáticamente
var user = _cache.GetOrSet("user:123", () =>
{
// Esto solo se ejecuta si no está en cache
return LoadUserFromDatabase(123);
}, TimeSpan.FromMinutes(30));
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly IMemoryCacheManager _cache;
private readonly IUserService _userService;
private readonly ILogger<UsersController> _logger;
public UsersController(
IMemoryCacheManager cache,
IUserService userService,
ILogger<UsersController> logger)
{
_cache = cache;
_userService = userService;
_logger = logger;
}
[HttpGet("{id}")]
public ActionResult<User> GetUser(int id)
{
try
{
var cacheKey = $"user:{id}";
var user = _cache.GetOrSet(cacheKey, () =>
{
_logger.LogInformation("Loading user {UserId} from service", id);
return _userService.GetUserById(id);
}, TimeSpan.FromMinutes(15));
return user != null ? Ok(user) : NotFound();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving user {UserId}", id);
return StatusCode(500, "Internal server error");
}
}
[HttpPut("{id}")]
public ActionResult UpdateUser(int id, [FromBody] User user)
{
try
{
_userService.UpdateUser(user);
// Actualizar cache con datos frescos
var cacheKey = $"user:{id}";
_cache.Set(cacheKey, user, TimeSpan.FromMinutes(15));
return Ok(user);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error updating user {UserId}", id);
return StatusCode(500, "Internal server error");
}
}
[HttpDelete("{id}")]
public ActionResult DeleteUser(int id)
{
try
{
_userService.DeleteUser(id);
// Remover del cache
_cache.Remove($"user:{id}");
return NoContent();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error deleting user {UserId}", id);
return StatusCode(500, "Internal server error");
}
}
[HttpPost("cache/clear/{id}")]
public ActionResult ClearUserCache(int id)
{
try
{
var cacheKey = $"user:{id}";
var existed = _cache.Exists(cacheKey);
if (existed)
{
_cache.Remove(cacheKey);
_logger.LogInformation("Cache cleared for user {UserId}", id);
return Ok(new { Message = $"Cache cleared for user {id}" });
}
return Ok(new { Message = $"User {id} was not cached" });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error clearing cache for user {UserId}", id);
return StatusCode(500, "Internal server error");
}
}
}
public class ProductService : IProductService
{
private readonly IMemoryCacheManager _cache;
private readonly IProductRepository _repository;
private readonly ILogger<ProductService> _logger;
public ProductService(
IMemoryCacheManager cache,
IProductRepository repository,
ILogger<ProductService> logger)
{
_cache = cache;
_repository = repository;
_logger = logger;
}
public Product? GetProductById(int id)
{
return _cache.GetOrSet($"product:{id}", () =>
{
_logger.LogDebug("Loading product {ProductId} from repository", id);
return _repository.GetById(id);
});
}
public List<Product> GetProductsByCategory(int categoryId)
{
return _cache.GetOrSet($"products:category:{categoryId}", () =>
{
_logger.LogDebug("Loading products for category {CategoryId}", categoryId);
return _repository.GetByCategory(categoryId);
}, TimeSpan.FromMinutes(10));
}
public List<Product> GetPopularProducts()
{
return _cache.GetOrSet("products:popular", () =>
{
_logger.LogDebug("Loading popular products from repository");
return _repository.GetPopularProducts();
}, TimeSpan.FromMinutes(5)); // Cache más corto para datos populares
}
public void UpdateProduct(Product product)
{
_repository.Update(product);
// Actualizar cache del producto individual
_cache.Set($"product:{product.Id}", product);
// Limpiar cache de categoría para asegurar consistencia
_cache.Remove($"products:category:{product.CategoryId}");
// Limpiar cache de productos populares
_cache.Remove("products:popular");
_logger.LogInformation("Product {ProductId} updated and cache refreshed", product.Id);
}
public void DeleteProduct(int productId)
{
var product = GetProductById(productId);
if (product == null) return;
_repository.Delete(productId);
// Limpiar todos los caches relacionados
_cache.Remove($"product:{productId}");
_cache.Remove($"products:category:{product.CategoryId}");
_cache.Remove("products:popular");
_logger.LogInformation("Product {ProductId} deleted and cache cleared", productId);
}
public bool ProductExists(int id)
{
// Usar verificación de cache primero para rendimiento
if (_cache.Exists($"product:{id}"))
return true;
// Fallback al repositorio
var exists = _repository.Exists(id);
if (exists)
{
// Cachear el producto para uso futuro
var product = _repository.GetById(id);
if (product != null)
{
_cache.Set($"product:{id}", product);
}
}
return exists;
}
public decimal GetProductPrice(int id)
{
// Ejemplo de cache específico para un campo
return _cache.GetOrSet($"product:price:{id}", () =>
{
var product = _repository.GetById(id);
return product?.Price ?? 0;
}, TimeSpan.FromMinutes(30));
}
}
public class CacheService : ICacheService
{
private readonly IMemoryCacheManager _cache;
private readonly ILogger<CacheService> _logger;
public CacheService(IMemoryCacheManager cache, ILogger<CacheService> logger)
{
_cache = cache;
_logger = logger;
}
public T? GetOrCreate<T>(string key, Func<T> factory, TimeSpan? expiration = null)
{
return _cache.GetOrSet(key, factory, expiration);
}
public async Task<T?> GetOrCreateAsync<T>(string key, Func<Task<T>> factory, TimeSpan? expiration = null)
{
// Para operaciones asíncronas, verificamos cache primero
var cached = _cache.Get<T>(key);
if (cached != null)
return cached;
// Ejecutar factory asíncrono
var result = await factory();
// Cachear resultado
if (result != null)
{
_cache.Set(key, result, expiration);
}
return result;
}
public void InvalidatePattern(string pattern)
{
// Para patrones como "user:*", "product:category:*"
// Esta implementación requiere tracking de keys (no incluido en la implementación base)
_logger.LogInformation("Pattern invalidation requested: {Pattern}", pattern);
// Implementación simple: remover keys conocidas que coincidan
// En una implementación completa, necesitarías tracking de keys
}
public Dictionary<string, object> GetCacheInfo()
{
return new Dictionary<string, object>
{
{ "CacheEnabled", true },
{ "Timestamp", DateTime.UtcNow }
};
}
}
public interface ICacheService
{
T? GetOrCreate<T>(string key, Func<T> factory, TimeSpan? expiration = null);
Task<T?> GetOrCreateAsync<T>(string key, Func<Task<T>> factory, TimeSpan? expiration = null);
void InvalidatePattern(string pattern);
Dictionary<string, object> GetCacheInfo();
}
La Expiración Deslizante reinicia el temporizador de expiración cada vez que se accede a un elemento:
Ejemplo: SlidingExpiration = 10 minutos
14:00 - Elemento almacenado → Expira a las 14:10
14:05 - Elemento accedido → Expira a las 14:15 (¡se reinicia!)
14:12 - Elemento accedido de nuevo → Expira a las 14:22 (¡se reinicia!)
[Sin acceso por 10 minutos] → El elemento expira
Casos de Uso:
✓ Sesiones de usuario (mantener usuarios activos en cache)
✓ Datos de referencia frecuentemente accedidos
✓ Limpieza inteligente (remueve elementos no usados automáticamente)
// Configurar opciones programáticamente
builder.Services.AddMemoryCacheService(options =>
{
options.Enabled = true;
options.DefaultExpirationMinutes = 45;
options.DefaultSlidingExpirationMinutes = 10;
options.SizeLimit = 5000;
options.CompactionPercentage = 0.30;
});
// Usar un nombre de sección de configuración diferente
var cacheSection = builder.Configuration.GetSection("CustomCacheSettings");
builder.Services.AddMemoryCacheService(cacheSection);
La librería maneja errores de forma elegante:
Enabled = false, todas las operaciones son no-ops// Las operaciones de cache son seguras incluso si el cache falla
var user = _cache.Get<User>("user:123"); // Retorna null si hay error
_cache.Set("key", value); // Loguea error pero continúa la ejecución
Crear claves jerárquicas y descriptivas:
✓ "user:profile:123"
✓ "product:details:456"
✓ "category:products:electronics"
✗ "data123"
✗ "temp_stuff"
Siempre verificar null al obtener del cache:
var user = _cache.Get<User>("user:123");
if (user == null)
{
// Manejar cache miss
user = LoadUserFromDatabase(123);
_cache.Set("user:123", user);
}
Más eficiente para escenarios de cache-or-create:
var data = _cache.GetOrSet("expensive:data", () => LoadExpensiveData());
Balance entre rendimiento y frescura de datos:
// Datos de referencia - expiración más larga
_cache.Set("countries", countries, TimeSpan.FromHours(24));
// Datos de usuario - expiración más corta
_cache.Set("user:123", user, TimeSpan.FromMinutes(15));
// Datos temporales - expiración muy corta
_cache.Set("temp:token", token, TimeSpan.FromMinutes(5));
Remover cache relacionado cuando los datos cambian:
public void UpdateUser(User user)
{
_repository.Update(user);
// Remover cache directo
_cache.Remove($"user:{user.Id}");
// Remover caches relacionados si es necesario
_cache.Remove($"users:department:{user.DepartmentId}");
_cache.Remove("users:active");
}
public List<Product> GetProductsByCategory(int categoryId)
{
return _cache.GetOrSet($"products:category:{categoryId}", () =>
{
return _repository.GetProductsByCategory(categoryId);
}, TimeSpan.FromMinutes(10));
}
// Invalidar cuando se agregue/modifique un producto
public void AddProduct(Product product)
{
_repository.Add(product);
// Invalidar lista de la categoría
_cache.Remove($"products:category:{product.CategoryId}");
}
El cache no funciona: Verificar Enabled = true en configuración
Problemas de memoria: Configurar SizeLimit y CompactionPercentage apropiados
Errores de configuración: Revisar mensajes de validación en los logs
Problemas de rendimiento: Monitorear ratios de hit/miss del cache
Habilitar logging de debug para solucionar comportamiento del cache:
{
"Logging": {
"LogLevel": {
"EV.MemoryCache": "Debug"
}
}
}
// Verificar configuración al inicio
public static void ValidateCache(IServiceProvider services)
{
using var scope = services.CreateScope();
var cache = scope.ServiceProvider.GetRequiredService<IMemoryCacheManager>();
// Prueba básica
cache.Set("test", "value", TimeSpan.FromMinutes(1));
var retrieved = cache.Get<string>("test");
if (retrieved == "value")
Console.WriteLine("✅ Cache working correctly");
else
Console.WriteLine("❌ Cache configuration issue");
cache.Remove("test");
}
Copyright © 2025 EVI. Todos los derechos reservados.
Cache en Memoria Simple, Rápido y Confiable para .NET 8