Caché de Datos en Entity Framework Core: Un Enfoque Práctico

En el post de hoy sobre el curso de entity framework vamos a ver cómo podemos optimizar nuestras aplicaciones utilizando caché en entity framework core.

 

1 - Qué es la caché? 

En este blog tengo un post muy detallado de qué es cache y como implementarla con .net y redis, pero en términos generales podemos decir que la caché es un lugar donde almacenamos información para consultarla de una forma muy rápida y eficiente. 

 

Comúnmente suele ser en memoria, ya bien sea en la propia memoria de la aplicación o servicio, o en una memoria más distribuida con Redis. 

 

 

2 - Por qué utilizar caché en entity framework core 

Antes de empezar, es fundamental entender por qué esto es importante. Imagina que tienes una aplicación con muchos usuarios, todos realizando la misma consulta. ¿Realmente queremos golpear la base de datos cada vez? Claro que no. Aquí es donde entra en juego el caché.

 

Dentro de Entity framework, tenemos dos tipos de caché;

  1. Caché de primer nivel: Por defecto Entity framework utiliza caché de primer nivel; Esto quiere decir que cada vez que haces una consulta, EF comprueba en su caché local para ver si ya tienes los datos, y si los tiene, los devuelve. Por supuesto si no los tiene, hace la consulta en la base de datos. 
  2. Caché de segundo nivel: La caché de segundo nivel es algo más compleja y NO viene incluida en EF por defecto. La idea de la caché de segundo nivel es que se puede compartir entre múltiples contextos, lo que quiere decir, que si una app tiene muchos usuarios, todos van a compartir esa caché, lo que en muchos escenarios, va a mejorar el rendimiento de la app. 

 

 

2.1 - Caché de primer nivel en entity framework core

Como punto de partida, vamos a ver un ejemplo muy sencillo; En el siguiente código: 

[HttpGet("cache-level1/{userId}")]
public async Task<User?> CacheLevel1(int userId)
{
    User? user =await _context.Users.FirstOrDefaultAsync(a => a.Id == userId);
    
    User? cachedUser =await _context.Users.FirstOrDefaultAsync(a => a.Id == userId);
    return cachedUser;
}

La primera consulta al contexto se realiza en la base de datos, mientras que la segunda, la obtiene directamente de la caché.

 

 

2.2 - Implementar caché de segundo nivel en entity framework Core

Ahora vamos a pasar a implementar caché de segundo nivel, una forma muy sencilla de implementar nosotros mismos la caché de segundo nivel es utilizando los interceptores que vimos en el post anterior. 

Por supuesto, puedes utilizar librerías de terceros, pero para nuestro ejemplo, para entender cómo funcionan esas librerías por detrás, lo vamos a hacer de forma manual. 

 

Lo primero que tenemos que hacer es instalar el paquete Microsoft.Extensions.Caching.Memory, y crear el interceptor correspondiente: 

public class SecondLevelCacheInterceptor : DbCommandInterceptor
{
    private readonly IMemoryCache _cache;

    public SecondLevelCacheInterceptor(IMemoryCache cache)
    {
        _cache = cache;
    }

    public override async ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(DbCommand command,
        CommandEventData eventData, InterceptionResult<DbDataReader> result,
        CancellationToken cancellationToken = default)
    {
        string key = command.CommandText +
                     string.Join(",", command.Parameters.Cast<DbParameter>().Select(p => p.Value));
        
        if (_cache.TryGetValue(key, out List<Dictionary<string, object>>? cacheEntry))
        {
            var table = new DataTable();
            if (cacheEntry != null && cacheEntry.Any())
            {
                Console.WriteLine("read from cache");
                foreach (var pair in cacheEntry.First())
                {
                    table.Columns.Add(pair.Key,
                        pair.Value is not null && pair.Value?.GetType() != typeof(DBNull)
                            ? pair.Value.GetType()
                            : typeof(object));
                }

                foreach (var row in cacheEntry)
                {
                    table.Rows.Add(row.Values.ToArray());
                }
            }

            var reader = table.CreateDataReader();
            Console.WriteLine("==== READ FROm CACHE ===");
            return InterceptionResult<DbDataReader>.SuppressWithResult(reader);
        }

        return result;
    }

    public override async ValueTask<DbDataReader> ReaderExecutedAsync(DbCommand command,
        CommandExecutedEventData eventData, DbDataReader result, CancellationToken cancellationToken = default)
    {
        var key = command.CommandText + string.Join(",", command.Parameters.Cast<DbParameter>().Select(p => p.Value));


        var resultsList = new List<Dictionary<string, object>>();
        if (result.HasRows)
        {
            while (await result.ReadAsync())
            {
                var row = new Dictionary<string, object>();
                for (var i = 0; i < result.FieldCount; i++)
                {
                    row.TryAdd(result.GetName(i), result.GetValue(i));
                }

                resultsList.Add(row);
            }

            if (resultsList.Any())
            {
                _cache.Set(key, resultsList);
            }
        }

        result.Close();
        
        var table = new DataTable();
        if (resultsList.Any())
        {
        
            foreach (var pair in resultsList.First())
            {
                table.Columns.Add(pair.Key,
                    pair.Value is not null && pair.Value?.GetType() != typeof(DBNull)
                        ? pair.Value.GetType()
                        : typeof(object));
            }

            foreach (var row in resultsList)
            {
                table.Rows.Add(row.Values.ToArray());
            }
        }

        return table.CreateDataReader();
    }
}

El código parece mucho más complejo de lo que realmente es, debido a cómo funciona el DataReader, pero al final simplemente estamos almacenando en caché y leyendo de la caché. Lógica sencillita. 

  • Nota: Esta caché funciona con TODAS las consultas, si tu aplicación requiere que solo algunas sean almacenadas en memoria, tendrás que implementar esa parte, o utilizar librerías de terceros. 



Una vez tenemos el interceptor tenemos que registrar tanto IMemoryCache. Por cierto, si quieres utilizar Redis en vez de la memoria, tendrás que registrar redis. 

builder.Services.AddMemoryCache();

 

Y posteriormente el interceptor:

public static void AddMySql(this IServiceCollection services)
{
    services.AddDbContext<CursoEfContext>((serviceProvider, options) =>
        options
            .UseLazyLoadingProxies()
            .AddInterceptors(new ReadExampleInterceptor(),
                new SecondLevelCacheInterceptor(serviceProvider.GetRequiredService<IMemoryCache>())) // <---- this
            .UseMySQL("server=127.0.0.1;port=4306;database=cursoEF;user=root;password=cursoEFpass"));
}

Y ya está, si ahora ejecutas el código, podrás ver cómo utilizamos la caché en la segunda llamada.

explanation second level caching entity framework core

Antes de terminar, no te olvides que este es un ejemplo sencillo, en un entorno de producción vas a tener que decidir la duración de la caché, la invalidación, si quieres tener los datos en memoria o en un servicio distribuido como redis.

Como digo, las librerías de terceros ya tienen dichas implementaciones, pero para entender el concepto es mejor si lo hace uno mismo.


Uso del bloqueador de anuncios adblock

Hola!

Primero de todo bienvenido a la web de NetMentor donde podrás aprender programación en C# y .NET desde un nivel de principiante hasta más avanzado.


Yo entiendo que utilices un bloqueador de anuncios como AdBlock, Ublock o el propio navegador Brave. Pero te tengo que pedir por favor que desactives el bloqueador para esta web.


Intento personalmente no poner mucha publicidad, la justa para pagar el servidor y por supuesto que no sea intrusiva; Si pese a ello piensas que es intrusiva siempre me puedes escribir por privado o por Twitter a @NetMentorTW.


Si ya lo has desactivado, por favor recarga la página.


Un saludo y muchas gracias por tu colaboración

© copyright 2024 NetMentor | Todos los derechos reservados | RSS Feed

Buy me a coffee Invitame a un café