Case 8: Stop Re-calculating - Caching Hot Data with IMemoryCache
The Scenario 📝
- System: An e-commerce website. The main navigation menu of the site displays a list of all product
Categories. - Problem: This list is shown on almost every page of the website. The data is nearly static, changing only a few times a day when an administrator adds or edits a category. However, the application is querying the database for this list on every single request.
The Problematic Code (Fetching Every Time)
csharp
public class NavigationService
{
private readonly AppDbContext _context;
public NavigationService(AppDbContext context)
{
_context = context;
}
public async Task<List<Category>> GetCategoriesAsync()
{
// This query runs thousands of times per minute under high load
return await _context.Categories
.AsNoTracking()
.OrderBy(c => c.Name)
.ToListAsync();
}
}The Hidden Bottleneck 🧐
- Unnecessary Database Load: Even though each individual query for categories is fast, running it thousands of times per minute creates a significant load on the database.
- Wasted Resources: Every query uses a database connection, consumes CPU, and adds unnecessary network latency to every request.
- The Core Problem: The application is constantly asking a question it already knows the answer to. The category data almost never changes.
The Solution: Use In-Memory Caching with IMemoryCache ✅
The Logic: We will use the "Cache-Aside" pattern.
- Check if the data is in the cache.
- If yes, return the data from the cache immediately.
- If no, get the data from the database, save it to the cache for next time, and then return it.
IMemoryCacheis a built-in service in ASP.NET Core that is very easy to use.The Optimized Code:
csharppublic class NavigationService { private readonly AppDbContext _context; private readonly IMemoryCache _cache; // Inject IMemoryCache private const string CategoriesCacheKey = "NavigationCategories"; public NavigationService(AppDbContext context, IMemoryCache cache) { _context = context; _cache = cache; } public async Task<List<Category>> GetCategoriesAsync() { // Use GetOrCreateAsync to implement the cache-aside pattern safely and cleanly return await _cache.GetOrCreateAsync(CategoriesCacheKey, async entry => { // Set a cache expiration time, for example, 1 hour entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1); // This logic only runs when the data is not in the cache (a cache miss) return await _context.Categories .AsNoTracking() .OrderBy(c => c.Name) .ToListAsync(); }); } }
The Results ✨
- Drastically Reduced Database Load: After the first request, for the next hour, 100% of all other requests will get the data from the application's memory without touching the database.
- Faster Response Times: Reading from RAM is many times faster than making a round-trip to the database. The website will load faster.
- Increased Scalability: The database is freed from repetitive queries, allowing it to use its resources for more important transactions.
Conclusion:
- Golden Rule: "Don't fetch what you already have."
- Caching is one of the most effective strategies for improving web application performance.
IMemoryCacheis a simple, built-in tool for caching "hot data"—data that is accessed frequently but changes rarely.