EV.MemoryCache 1.0.0

dotnet add package EV.MemoryCache --version 1.0.0
                    
NuGet\Install-Package EV.MemoryCache -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="EV.MemoryCache" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="EV.MemoryCache" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="EV.MemoryCache" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add EV.MemoryCache --version 1.0.0
                    
#r "nuget: EV.MemoryCache, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package EV.MemoryCache@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=EV.MemoryCache&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=EV.MemoryCache&version=1.0.0
                    
Install as a Cake Tool

EV.MemoryCache

Librerí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.


Características

  • Integración Fácil: Configuración simple con inyección de dependencias
  • Expiración Configurable: Soporte para tiempos de expiración absolutos y deslizantes
  • Gestión de Memoria: Límites de tamaño y compactación automática
  • Logging Comprehensivo: Logging detallado con niveles configurables
  • Validación: Validación de configuración integrada
  • Optimizado para Rendimiento: Operaciones síncronas para acceso rápido en memoria
  • Listo para Producción: Manejo de errores y degradación elegante

Instalación

dotnet add package EV.MemoryCache

Inicio Rápido

1. Configuración (appsettings.json)

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "EV.MemoryCache": "Debug"
    }
  },
  "AllowedHosts": "*",
  "MemoryCacheConfiguration": {
    "Enabled": true,
    "DefaultExpirationMinutes": 30,
    "DefaultSlidingExpirationMinutes": 5,
    "SizeLimit": 3000,
    "CompactionPercentage": 0.25
  }
}

2. Registro de Servicios (Program.cs)

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();

3. Uso en Servicios

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}");
    }
}

Opciones de Configuración

Propiedades de MemoryCacheConfiguration

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)

Ejemplos de Configuración

Ambiente de Desarrollo
{
  "MemoryCacheConfiguration": {
    "Enabled": true,
    "DefaultExpirationMinutes": 5,
    "DefaultSlidingExpirationMinutes": 2,
    "SizeLimit": 100
  }
}
Ambiente de Producción
{
  "MemoryCacheConfiguration": {
    "Enabled": true,
    "DefaultExpirationMinutes": 60,
    "DefaultSlidingExpirationMinutes": 15,
    "SizeLimit": 50000,
    "CompactionPercentage": 0.20
  }
}
Cache Deshabilitado (para testing)
{
  "MemoryCacheConfiguration": {
    "Enabled": false
  }
}

Referencia de API

IMemoryCacheManager

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

Patrones de Uso

Operaciones Básicas de Cache
// 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 Get-or-Set (Recomendado)
// 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));

Uso Avanzado

Implementación en Controladores

[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");
        }
    }
}

Capa de Servicio con Cache

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));
    }
}

Servicio de Cache Genérico

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();
}

Entendiendo la Expiración Deslizante

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)

Métodos Alternativos de Registro

Configuración mediante Delegate

// Configurar opciones programáticamente
builder.Services.AddMemoryCacheService(options =>
{
    options.Enabled = true;
    options.DefaultExpirationMinutes = 45;
    options.DefaultSlidingExpirationMinutes = 10;
    options.SizeLimit = 5000;
    options.CompactionPercentage = 0.30;
});

Configuración desde Sección Específica

// Usar un nombre de sección de configuración diferente
var cacheSection = builder.Configuration.GetSection("CustomCacheSettings");
builder.Services.AddMemoryCacheService(cacheSection);

Manejo de Errores

La librería maneja errores de forma elegante:

  • Cache Deshabilitado: Cuando Enabled = false, todas las operaciones son no-ops
  • Operaciones Get: Retornan null si el cache no está disponible
  • Operaciones Set: Loguean errores pero no lanzan excepciones
  • Validación: La configuración es validada al inicio
// 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

Mejores Prácticas

1. Usar Claves Significativas

Crear claves jerárquicas y descriptivas:

✓ "user:profile:123"
✓ "product:details:456"
✓ "category:products:electronics"
✗ "data123"
✗ "temp_stuff"

2. Manejar Valores Null

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);
}

3. Usar Patrón GetOrSet

Más eficiente para escenarios de cache-or-create:

var data = _cache.GetOrSet("expensive:data", () => LoadExpensiveData());

4. Considerar Tiempos de Expiración

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));

5. Invalidación de Cache

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");
}

6. Cache de Listas y Colecciones

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}");
}

Solución de Problemas

Problemas Comunes

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

Logging

Habilitar logging de debug para solucionar comportamiento del cache:

{
  "Logging": {
    "LogLevel": {
      "EV.MemoryCache": "Debug"
    }
  }
}

Validación de Configuración

// 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");
}

Requisitos

  • .NET 8.0 o superior
  • Microsoft.Extensions.Caching.Memory 8.0+
  • Microsoft.Extensions.Configuration 8.0+
  • Microsoft.Extensions.DependencyInjection 8.0+
  • Microsoft.Extensions.Logging 8.0+
  • Microsoft.Extensions.Options 8.0+

Licencia

Copyright © 2025 EVI. Todos los derechos reservados.


Cache en Memoria Simple, Rápido y Confiable para .NET 8

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 195 8/27/2025

🎉 Lanzamiento Inicial 1.0.0

✨ Características de Memoria Caché:
• Sistema de caché en memoria de alto rendimiento
• Gestión automática de expiración temporal y absoluta
• Soporte para serialización automática de objetos complejos
• Decoradores para logging de operaciones de caché
• Invalidación inteligente por patrones y tags
• Configuración flexible a través de appsettings.json
• Soporte para cache particionado y distribuído