Course contentsShow
← C#
Lesson 1153 of 1,90810. Entity Framework Core ModelingFree lesson

Advanced Caching Strategies

Implement query-level caching to reduce database hits for frequently accessed data.

In this lesson, you will learn how to implement query-level caching to intercept Entity Framework Core execution and drastically reduce database round-trips for frequently accessed data.

The Problem with Direct Queries

By default, every time you execute a LINQ query in EF Core, the database provider translates that expression into SQL and sends it to the database engine. For "hot" data—such as site settings, static configuration, or frequently viewed product categories—repeatedly hitting the database adds unnecessary latency and load.

Implementing Query-Level Caching

To optimize this, we implement a "Cache-Aside" pattern at the query level. Instead of querying the DbContext directly, we check an IMemoryCache first. If the data isn't there, we query the database, store the result in the cache for a specific duration, and then return it.

Here is how you can implement this pattern cleanly:

public async Task<List<Product>> GetFeaturedProductsAsync(MyDbContext context, IMemoryCache cache)
{
    const string cacheKey = "featured_products";

    if (!cache.TryGetValue(cacheKey, out List<Product> products))
    {
        // Data not in cache, hit the database
        products = await context.Products
            .Where(p => p.IsFeatured)
            .AsNoTracking() // Always use AsNoTracking for cached read-only results
            .ToListAsync();

        // Save into cache for 10 minutes
        var cacheEntryOptions = new MemoryCacheEntryOptions()
            .SetAbsoluteExpiration(TimeSpan.FromMinutes(10));

        cache.Set(cacheKey, products, cacheEntryOptions);
    }

    return products;
}

Critical Considerations

When implementing query-level caching, keep two things in mind:

  1. AsNoTracking() is Essential: When you cache entities, you are caching objects that are detached from the DbContext tracker. If you attempt to modify these cached entities and attach them to a new DbContext without caution, you can encounter tracking errors. For read-heavy, cached data, always use .AsNoTracking().
  2. Stale Data: Caching introduces the risk of serving outdated information. Ensure your expiration policy aligns with your business requirements for data freshness, or implement a cache-invalidation strategy whenever an update occurs to that specific dataset.

By intercepting these requests before they reach the database driver, you protect your infrastructure during traffic spikes and keep your application responsive.

Key Takeaway: Implement the Cache-Aside pattern using IMemoryCache combined with AsNoTracking() queries to serve static or semi-static data from RAM instead of the database.