Data Caching in Entity Framework Core: A Practical Approach

In today’s post about the Entity Framework course, we will look at how we can optimize our applications by using caching in Entity Framework Core.

 

1 - What is cache?

On this blog, I have a very detailed post about what caching is and how to implement it with .net and redis, but generally speaking, we can say that cache is a place where we store information to retrieve it in a very fast and efficient way.

 

Usually, this cache is in memory, either in the local memory of the application or service, or in more distributed memory using Redis.

 

 

2 - Why use cache in Entity Framework Core

Before we begin, it’s essential to understand why this matters. Imagine you have an application with many users, all making the same query. Do we really want to hit the database every single time? Of course not. This is where cache comes into play.

 

Within Entity Framework, we have two types of cache:

  1. First level cache: By default, Entity Framework uses first level cache. This means that every time you make a query, EF checks its local cache to see if it already has the data, and if it does, it returns it. Of course, if it doesn’t, it goes to the database.
  2. Second level cache: Second level cache is a bit more complex and is NOT included in EF by default. The idea of second level cache is that it can be shared between multiple contexts, which means that if an app has many users, they all share that cache, which in many scenarios will improve the app’s performance.

 

 

2.1 - First level cache in Entity Framework Core

As a starting point, let’s look at a very simple example. In the following code:

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

The first query to the context is done in the database, while the second retrieves it directly from the cache.

 

 

2.2 - Implementing second level cache in Entity Framework Core

Now, let’s implement second level cache. A very simple way to do this ourselves is by using the interceptors we saw in the previous post.

Of course, you can use third-party libraries, but for our example, to understand how these libraries work under the hood, we’ll do it manually.

 

The first thing we have to do is install the Microsoft.Extensions.Caching.Memory package and create the corresponding interceptor:

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

The code looks much more complicated than it actually is because of the way DataReader works, but in the end we are simply storing in the cache and reading from the cache. Simple logic.

  • Note: This cache works with ALL queries. If your application requires only some queries to be stored in memory, you’ll need to implement that part or use third-party libraries.



Once we have the interceptor, we need to register both IMemoryCache. By the way, if you want to use Redis instead of memory, you’ll need to register Redis.

builder.Services.AddMemoryCache();

 

And next, the 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"));
}

And that’s it! If you now run the code, you can see how we use the cache in the second call.

explanation second level caching entity framework core

Before you finish, remember that this is a simple example. In a production environment, you’ll have to make decisions about cache duration, invalidation, and whether you want to keep data in memory or in a distributed service like Redis.

As I said before, third-party libraries already have those features implemented, but to understand the concept, it’s better to do it yourself at least once.

This post was translated from Spanish. You can see the original one here.
If there is any problem you can add a comment bellow or contact me in the website's contact form

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 2025 NetMentor | Todos los derechos reservados | RSS Feed

Buy me a coffee Invitame a un café