Métodos de extensión personalizados para caché distribuida en .NET Core

La interfaz IDistributedCache en .NET Core ofrece métodos básicos como GetString y SetString, pero en la práctica se requiere lógica adicional para serializar y deserializar objetos. Para simplificar este proceso, se pueden crear métodos de extensión que encapsulen esa funcionalidad.

Inspiración en IMemoryCache.GetOrCreate

El método GetOrCreate de IMemoryCache permite obtener un valor de la caché o, si no existe, ejecutar una función para generarlo y almacenarlo. Su implementación interna es la siguiente:

public static TItem GetOrCreate<TItem>(this IMemoryCache cache, object key, Func<ICacheEntry, TItem> factory)
{
    if (!cache.TryGetValue(key, out object result))
    {
        ICacheEntry entry = cache.CreateEntry(key);
        result = factory(entry);
        entry.SetValue(result);
        entry.Dispose();
    }

    return (TItem)result;
}

El patrón es directo: intentar leer, y si falla, crear el dato y guardarlo. Se puede replicar este comportamiento para caché distribuida.

Diseño de los métodos de extensión

La siguiente clase estática define un conjunto de extensiones para IDistributedCache que manejan automáticamente la serialización JSON. Se establece una expiración absoluta por defecto de 30 minutos.

public static class CacheDistribuidoExtensiones
{
    private static readonly DistributedCacheEntryOptions OpcionesPorDefecto =
        new DistributedCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromMinutes(30));

    public static T Obtener<T>(this IDistributedCache cache, string clave)
    {
        try
        {
            string json = cache.GetString(clave);
            if (string.IsNullOrEmpty(json))
                return default;

            return JsonSerializer.Deserialize<T>(json);
        }
        catch
        {
            return default;
        }
    }

    public static async Task<T> ObtenerAsync<T>(
        this IDistributedCache cache, string clave, CancellationToken ct = default)
    {
        try
        {
            string json = await cache.GetStringAsync(clave, ct);
            if (string.IsNullOrEmpty(json))
                return default;

            return JsonSerializer.Deserialize<T>(json);
        }
        catch
        {
            return default;
        }
    }

    public static bool IntentarObtener<T>(
        this IDistributedCache cache, string clave, out T valor)
    {
        string json = cache.GetString(clave);
        if (!string.IsNullOrEmpty(json))
        {
            valor = JsonSerializer.Deserialize<T>(json);
            return true;
        }
        valor = default;
        return false;
    }

    public static void Guardar<T>(
        this IDistributedCache cache, string clave, T valor)
    {
        cache.SetString(clave, JsonSerializer.Serialize(valor), OpcionesPorDefecto);
    }

    public static void Guardar<T>(
        this IDistributedCache cache, string clave, T valor,
        DistributedCacheEntryOptions opciones)
    {
        cache.SetString(clave, JsonSerializer.Serialize(valor), opciones);
    }

    public static async Task GuardarAsync<T>(
        this IDistributedCache cache, string clave, T valor,
        CancellationToken ct = default)
    {
        await cache.SetStringAsync(
            clave, JsonSerializer.Serialize(valor), OpcionesPorDefecto, ct);
    }

    public static async Task GuardarAsync<T>(
        this IDistributedCache cache, string clave, T valor,
        DistributedCacheEntryOptions opciones, CancellationToken ct = default)
    {
        await cache.SetStringAsync(
            clave, JsonSerializer.Serialize(valor), opciones, ct);
    }

    public static T ObtenerOcrear<T>(
        this IDistributedCache cache, string clave, Func<T> fabrica)
    {
        if (!cache.IntentarObtener(clave, out T dato))
        {
            dato = fabrica();
            cache.Guardar(clave, dato);
        }
        return dato;
    }

    public static T ObtenerOcrear<T>(
        this IDistributedCache cache, string clave,
        Func<T> fabrica, DistributedCacheEntryOptions opciones)
    {
        if (!cache.IntentarObtener(clave, out T dato))
        {
            dato = fabrica();
            cache.Guardar(clave, dato, opciones);
        }
        return dato;
    }

    public static async Task<T> ObtenerOcrearAsync<T>(
        this IDistributedCache cache, string clave, Func<Task<T>> fabrica)
    {
        if (!cache.IntentarObtener(clave, out T dato))
        {
            dato = await fabrica();
            await cache.GuardarAsync(clave, dato);
        }
        return dato;
    }

    public static async Task<T> ObtenerOcrearAsync<T>(
        this IDistributedCache cache, string clave,
        Func<Task<T>> fabrica, DistributedCacheEntryOptions opciones)
    {
        if (!cache.IntentarObtener(clave, out T dato))
        {
            dato = await fabrica();
            await cache.GuardarAsync(clave, dato, opciones);
        }
        return dato;
    }
}

Ejemplos de uso

Inyección del servicio en una clase de dominio:

private readonly IDistributedCache _cacheDistribuido;

public ServicioCategoria(IDistributedCache cacheDistribuido)
{
    _cacheDistribuido = cacheDistribuido;
}

public async Task<List<CategoriaDto>> ObtenerCategorias()
{
    return await _cacheDistribuido.ObtenerOcrearAsync(
        ClavesCache.ListaCategorias,
        async () => await ConsultarServicioExterno());
}

Operaciones de lectura directa:

var codigo = _cacheDistribuido.Obtener<string>(clave);
var pedido = _cacheDistribuido.Obtener<Pedido>(clave);

Etiquetas: idistributedcache .NET Core caching C# extension methods

Publicado el 7-10 03:01