Hitting database for same data 1000 times per second? IMemoryCache stores frequently-accessed data in RAM for instant access.
Setup:
// Program.cs or Startup.cs builder.Services.AddMemoryCache();
Basic Usage:
public class ProductService
{
private readonly IMemoryCache _cache;
private readonly ApplicationDbContext _db;
public ProductService(IMemoryCache cache, ApplicationDbContext db)
{
_cache = cache;
_db = db;
}
public async Task GetProductAsync(int id)
{
// Try get from cache
if (_cache.TryGetValue($"product_{id}", out Product product))
{
return product; // Cache hit - instant return!
}
// Cache miss - query database
product = await _db.Products.FindAsync(id);
// Store in cache for 10 minutes
_cache.Set($"product_{id}", product, TimeSpan.FromMinutes(10));
return product;
}
}
// First call: 50ms (database query)
// Next 1000 calls: 0.1ms each (from cache)
// = 500x faster!
Advanced – Get or Create Pattern:
public async Task> GetProductsByCategoryAsync(int categoryId) { return await _cache.GetOrCreateAsync($"products_category_{categoryId}", async entry => { // Set cache options entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15); entry.SlidingExpiration = TimeSpan.FromMinutes(5); entry.Priority = CacheItemPriority.Normal; // This only runs if cache is empty return await _db.Products .Where(p => p.CategoryId == categoryId) .ToListAsync(); }); } // Clean syntax - cache management handled automatically
Cache Expiration Strategies:
// Absolute Expiration: Cache expires after fixed time
_cache.Set("key", value, new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30)
// Expires 30 minutes from now, regardless of usage
});
// Sliding Expiration: Resets on every access
_cache.Set("key", value, new MemoryCacheEntryOptions
{
SlidingExpiration = TimeSpan.FromMinutes(10)
// Expires 10 minutes after LAST access
// If accessed again, timer resets
});
// Combine both: Max lifetime with sliding
_cache.Set("key", value, new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1), // Max 1 hour
SlidingExpiration = TimeSpan.FromMinutes(10) // 10 min idle timeout
// Expires after 1 hour OR 10 minutes of inactivity
});
// Size Limit: Prevent cache from growing too large
_cache.Set("key", value, new MemoryCacheEntryOptions
{
Size = 1, // This item counts as size 1
Priority = CacheItemPriority.Low // Evict this first if memory low
});
Cache Invalidation on Update:
public async Task UpdateProductAsync(Product product)
{
await _db.SaveChangesAsync();
// Remove from cache so next access gets fresh data
_cache.Remove($"product_{product.Id}");
// Also remove list caches containing this product
_cache.Remove($"products_category_{product.CategoryId}");
}
// Cache stays in sync with database
Callback on Eviction:
_cache.Set("important_data", value, new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30),
PostEvictionCallbacks =
{
new PostEvictionCallbackRegistration
{
EvictionCallback = (key, value, reason, state) =>
{
_logger.LogInformation($"Cache key {key} evicted. Reason: {reason}");
if (reason == EvictionReason.Expired)
{
// Refresh cache proactively
RefreshCache((string)key);
}
}
}
}
});
Performance Comparison – API Endpoint:
// Without cache:
[HttpGet("products/{id}")]
public async Task GetProduct(int id)
{
var product = await _db.Products.FindAsync(id);
return Ok(product);
}
// Load test: 100 requests/sec
// Average response: 50ms
// Database load: 100 queries/sec
// With cache:
[HttpGet("products/{id}")]
public async Task GetProduct(int id)
{
var product = await _cache.GetOrCreateAsync($"product_{id}", async entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10);
return await _db.Products.FindAsync(id);
});
return Ok(product);
}
// Load test: 100 requests/sec
// Average response: 2ms (25x faster!)
// Database load: 0.1 queries/sec (first load only)
Cache Size Management:
// Configure size limit
builder.Services.AddMemoryCache(options =>
{
options.SizeLimit = 1024; // Total size limit
options.CompactionPercentage = 0.2; // Remove 20% when limit hit
});
// Specify size for each entry
_cache.Set("large_object", data, new MemoryCacheEntryOptions
{
Size = 100 // This item counts as 100 units
});
_cache.Set("small_object", data, new MemoryCacheEntryOptions
{
Size = 1
});
// When total size exceeds 1024, lowest priority items evicted first
Distributed Cache Alternative (Redis):
// IMemoryCache: In-process, lost on restart
// IDistributedCache: Shared across servers, persists
// For single server: Use IMemoryCache (faster)
// For multiple servers: Use IDistributedCache (Redis, SQL Server)
// Setup Redis distributed cache:
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost:6379";
});
// Usage is similar:
private readonly IDistributedCache _cache;
var cachedData = await _cache.GetStringAsync("key");
if (cachedData == null)
{
var data = await GetFromDatabase();
await _cache.SetStringAsync("key", JsonSerializer.Serialize(data),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
});
}
When to Use IMemoryCache:
✅ Use IMemoryCache for: - Frequently accessed, rarely changed data - Single-server applications - Sub-second response times needed - Small to medium datasets (< 100MB) - Configuration data, lookup tables ❌ Don't use for: - Data that changes frequently - Multi-server deployments (use Redis) - User session data (use cookies/Redis) - Very large datasets (> 1GB) - Data that must persist across restarts
