ASP.NET Core Caching Strategies Epic: Master Redis, Memory Caching & Performance Optimization
Ultimate guide to ASP.NET Core caching: Redis, memory caching, distributed patterns, real-world examples, and performance optimization for high-traffic apps.
ASPNETCore,Caching,Redis,Performance,MemoryCache,DistributedCache,Optimization,BestPractices,HighTraffic,WebDevelopment
Module 27: Caching Strategies Epic - Master ASP.NET Core Caching for High-Performance Applications
Table of Contents
1. The Caching Revolution
Why Caching is Your Performance Silver Bullet
Caching transforms application performance by reducing database load, decreasing response times, and improving scalability. In modern web applications, caching isn't just an optimization—it's a necessity.
Real-Life Analogy: Imagine a busy coffee shop. Without caching, every customer order would require:
Going to the supplier warehouse (database)
Selecting beans (query execution)
Grinding fresh (data processing)
Brewing individually (response generation)
With caching, popular drinks are pre-prepared and ready to serve instantly!
// Performance impact demonstration public class ProductServiceWithoutCaching { private readonly ApplicationDbContext _context; public async Task<Product> GetProductAsync(int id) { // Every call hits the database - SLOW! return await _context.Products .Include(p => p.Category) .Include(p => p.Reviews) .FirstOrDefaultAsync(p => p.Id == id); } } public class ProductServiceWithCaching { private readonly ApplicationDbContext _context; private readonly IMemoryCache _cache; public async Task<Product> GetProductAsync(int id) { // Try cache first - FAST! if (_cache.TryGetValue($"product_{id}", out Product product)) return product; // Cache miss - get from database product = await _context.Products .Include(p => p.Category) .Include(p => p.Reviews) .FirstOrDefaultAsync(p => p.Id == id); // Store in cache for future requests _cache.Set($"product_{id}", product, TimeSpan.FromMinutes(30)); return product; } }
The Caching Performance Impact
| Scenario | Without Caching | With Caching | Improvement |
|---|---|---|---|
| Database Queries | 1000 queries/sec | 50 queries/sec | 20x reduction |
| Response Time | 200ms | 5ms | 40x faster |
| Server Load | 80% CPU | 20% CPU | 4x reduction |
| Cost | $1000/month | $250/month | 75% savings |
2. Caching Fundamentals & Architecture
Caching Architecture Patterns
// Comprehensive caching service interface public interface ICacheService { Task<T> GetOrCreateAsync<T>(string key, Func<Task<T>> factory, TimeSpan? expiration = null); Task<T> GetAsync<T>(string key); Task SetAsync<T>(string key, T value, TimeSpan? expiration = null); Task RemoveAsync(string key); Task<bool> ExistsAsync(string key); Task RemoveByPatternAsync(string pattern); } // Implementation supporting multiple cache providers public class CacheService : ICacheService { private readonly IMemoryCache _memoryCache; private readonly IDistributedCache _distributedCache; private readonly ILogger<CacheService> _logger; private readonly CacheSettings _settings; public CacheService(IMemoryCache memoryCache, IDistributedCache distributedCache, ILogger<CacheService> logger, IOptions<CacheSettings> settings) { _memoryCache = memoryCache; _distributedCache = distributedCache; _logger = logger; _settings = settings.Value; } public async Task<T> GetOrCreateAsync<T>(string key, Func<Task<T>> factory, TimeSpan? expiration = null) { try { // Try memory cache first (fastest) if (_memoryCache.TryGetValue(key, out T cachedValue)) { _logger.LogDebug("Memory cache hit for key: {Key}", key); return cachedValue; } // Try distributed cache var distributedValue = await _distributedCache.GetAsync<T>(key); if (distributedValue != null) { _logger.LogDebug("Distributed cache hit for key: {Key}", key); // Populate memory cache for faster subsequent access var memoryCacheOptions = new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = expiration ?? _settings.DefaultExpiration }; _memoryCache.Set(key, distributedValue, memoryCacheOptions); return distributedValue; } // Cache miss - execute factory method _logger.LogDebug("Cache miss for key: {Key}, executing factory", key); var value = await factory(); if (value != null) { // Store in both caches var cacheExpiration = expiration ?? _settings.DefaultExpiration; // Memory cache var memoryOptions = new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = cacheExpiration }; _memoryCache.Set(key, value, memoryOptions); // Distributed cache var distributedOptions = new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = cacheExpiration }; await _distributedCache.SetAsync(key, value, distributedOptions); } return value; } catch (Exception ex) { _logger.LogError(ex, "Error in GetOrCreateAsync for key: {Key}", key); // If caching fails, fall back to factory method return await factory(); } } public async Task<T> GetAsync<T>(string key) { // Implementation similar to above without factory fallback if (_memoryCache.TryGetValue(key, out T memoryValue)) return memoryValue; var distributedValue = await _distributedCache.GetAsync<T>(key); if (distributedValue != null) { // Populate memory cache _memoryCache.Set(key, distributedValue, TimeSpan.FromMinutes(_settings.MemoryCacheExpirationMinutes)); return distributedValue; } return default(T); } public async Task SetAsync<T>(string key, T value, TimeSpan? expiration = null) { var cacheExpiration = expiration ?? _settings.DefaultExpiration; // Memory cache _memoryCache.Set(key, value, cacheExpiration); // Distributed cache await _distributedCache.SetAsync(key, value, new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = cacheExpiration }); } public async Task RemoveAsync(string key) { _memoryCache.Remove(key); await _distributedCache.RemoveAsync(key); _logger.LogInformation("Cache removed for key: {Key}", key); } public async Task<bool> ExistsAsync(string key) { return _memoryCache.TryGetValue(key, out _) || await _distributedCache.GetAsync(key) != null; } // Pattern-based removal for cache invalidation public async Task RemoveByPatternAsync(string pattern) { // This is a simplified version - actual implementation depends on cache provider _logger.LogInformation("Removing cache entries matching pattern: {Pattern}", pattern); // In real implementation, you'd use Redis keys command or similar // This is a conceptual implementation } } // Configuration model public class CacheSettings { public TimeSpan DefaultExpiration { get; set; } = TimeSpan.FromMinutes(30); public int MemoryCacheExpirationMinutes { get; set; } = 10; public string RedisConnectionString { get; set; } public bool UseDistributedCache { get; set; } = true; }
Cache Configuration in Program.cs
// Program.cs - Comprehensive caching setup var builder = WebApplication.CreateBuilder(args); // Configure cache settings builder.Services.Configure<CacheSettings>(builder.Configuration.GetSection("CacheSettings")); // Add memory cache (always available) builder.Services.AddMemoryCache(options => { options.SizeLimit = 1024 * 1024 * 100; // 100MB limit options.CompactionPercentage = 0.25; // Compact when 25% full }); // Add distributed cache based on configuration var cacheSettings = builder.Configuration.GetSection("CacheSettings").Get<CacheSettings>(); if (cacheSettings.UseDistributedCache && !string.IsNullOrEmpty(cacheSettings.RedisConnectionString)) { builder.Services.AddStackExchangeRedisCache(options => { options.Configuration = cacheSettings.RedisConnectionString; options.InstanceName = "MyApp:"; }); // Register Redis connection for direct access builder.Services.AddSingleton<IConnectionMultiplexer>(sp => ConnectionMultiplexer.Connect(cacheSettings.RedisConnectionString)); } else { // Fallback to distributed memory cache (for development) builder.Services.AddDistributedMemoryCache(); } // Register caching services builder.Services.AddScoped<ICacheService, CacheService>(); builder.Services.AddScoped<IProductCacheService, ProductCacheService>(); builder.Services.AddScoped<IUserCacheService, UserCacheService>(); // Add response caching builder.Services.AddResponseCaching(options => { options.MaximumBodySize = 1024 * 1024; // 1MB options.UseCaseSensitivePaths = false; }); // Add output caching (ASP.NET Core 7+) builder.Services.AddOutputCache(options => { options.AddBasePolicy(builder => builder.Expire(TimeSpan.FromMinutes(10))); options.AddPolicy("Products", builder => builder.Expire(TimeSpan.FromMinutes(5)) .Tag("products")); }); var app = builder.Build(); // Use response caching middleware app.UseResponseCaching(); // Use output caching middleware app.UseOutputCache(); app.Run();
3. In-Memory Caching Deep Dive
Advanced Memory Cache Patterns
// Smart memory cache service with eviction policies public class SmartMemoryCacheService { private readonly IMemoryCache _cache; private readonly ILogger<SmartMemoryCacheService> _logger; private readonly ConcurrentDictionary<string, CacheEntryInfo> _cacheEntries; public SmartMemoryCacheService(IMemoryCache cache, ILogger<SmartMemoryCacheService> logger) { _cache = cache; _logger = logger; _cacheEntries = new ConcurrentDictionary<string, CacheEntryInfo>(); } public async Task<T> GetOrCreateAsync<T>(string key, Func<Task<T>> factory, CacheOptions options = null) { options ??= new CacheOptions(); if (_cache.TryGetValue(key, out T cachedValue)) { // Update access statistics UpdateAccessStats(key); _logger.LogDebug("Cache hit for {Key}", key); return cachedValue; } // Cache miss - use factory method _logger.LogDebug("Cache miss for {Key}, executing factory", key); // Implement cache stampede protection var semaphore = new SemaphoreSlim(1, 1); await semaphore.WaitAsync(); try { // Double-check after acquiring lock if (_cache.TryGetValue(key, out cachedValue)) { UpdateAccessStats(key); return cachedValue; } var value = await factory(); if (value != null) { var cacheEntryOptions = CreateCacheEntryOptions(options); // Register callback for eviction cacheEntryOptions.RegisterPostEvictionCallback(EvictionCallback); _cache.Set(key, value, cacheEntryOptions); // Track cache entry _cacheEntries[key] = new CacheEntryInfo { Key = key, Created = DateTime.UtcNow, LastAccessed = DateTime.UtcNow, AccessCount = 1, Size = EstimateSize(value), Options = options }; } return value; } finally { semaphore.Release(); } } public CacheStatistics GetStatistics() { var entries = _cacheEntries.Values.ToList(); return new CacheStatistics { TotalEntries = entries.Count, TotalSize = entries.Sum(e => e.Size), HitRate = CalculateHitRate(), MostAccessed = entries.OrderByDescending(e => e.AccessCount).Take(10), OldestEntries = entries.OrderBy(e => e.LastAccessed).Take(10) }; } public void Cleanup() { var now = DateTime.UtcNow; var toRemove = new List<string>(); foreach (var entry in _cacheEntries) { var age = now - entry.Value.LastAccessed; if (age > entry.Value.Options.MaxIdleTime) { toRemove.Add(entry.Key); } } foreach (var key in toRemove) { _cache.Remove(key); _cacheEntries.TryRemove(key, out _); _logger.LogInformation("Removed idle cache entry: {Key}", key); } } private void UpdateAccessStats(string key) { if (_cacheEntries.TryGetValue(key, out var info)) { info.LastAccessed = DateTime.UtcNow; info.AccessCount++; } } private void EvictionCallback(object key, object value, EvictionReason reason, object state) { _logger.LogInformation("Cache entry evicted: {Key}, Reason: {Reason}", key, reason); _cacheEntries.TryRemove(key.ToString(), out _); } private MemoryCacheEntryOptions CreateCacheEntryOptions(CacheOptions options) { var cacheOptions = new MemoryCacheEntryOptions { Size = options.Size }; if (options.AbsoluteExpiration.HasValue) cacheOptions.SetAbsoluteExpiration(options.AbsoluteExpiration.Value); if (options.SlidingExpiration.HasValue) cacheOptions.SetSlidingExpiration(options.SlidingExpiration.Value); if (options.Priority.HasValue) cacheOptions.SetPriority(options.Priority.Value); return cacheOptions; } private long EstimateSize<T>(T value) { // Simple size estimation - in production, use more accurate methods if (value == null) return 0; try { using var stream = new MemoryStream(); var formatter = new BinaryFormatter(); formatter.Serialize(stream, value); return stream.Length; } catch { return 1024; // Default 1KB estimate } } private double CalculateHitRate() { var entries = _cacheEntries.Values.ToList(); if (entries.Count == 0) return 0; var totalAccesses = entries.Sum(e => e.AccessCount); var hits = entries.Sum(e => e.AccessCount - 1); // First access is always miss return totalAccesses > 0 ? (double)hits / totalAccesses : 0; } } // Supporting classes public class CacheOptions { public TimeSpan? AbsoluteExpiration { get; set; } public TimeSpan? SlidingExpiration { get; set; } public TimeSpan MaxIdleTime { get; set; } = TimeSpan.FromHours(24); public long Size { get; set; } = 1; public CacheItemPriority? Priority { get; set; } } public class CacheEntryInfo { public string Key { get; set; } public DateTime Created { get; set; } public DateTime LastAccessed { get; set; } public long AccessCount { get; set; } public long Size { get; set; } public CacheOptions Options { get; set; } } public class CacheStatistics { public int TotalEntries { get; set; } public long TotalSize { get; set; } public double HitRate { get; set; } public IEnumerable<CacheEntryInfo> MostAccessed { get; set; } public IEnumerable<CacheEntryInfo> OldestEntries { get; set; } }
Real-World Memory Cache Implementation
// E-commerce product catalog with intelligent caching public class ProductCatalogService { private readonly IProductRepository _productRepository; private readonly SmartMemoryCacheService _cache; private readonly ILogger<ProductCatalogService> _logger; private const string ProductsByCategoryKey = "products_category_{0}"; private const string FeaturedProductsKey = "products_featured"; private const string ProductDetailsKey = "product_{0}"; private const string ProductSearchKey = "products_search_{0}"; public ProductCatalogService(IProductRepository productRepository, SmartMemoryCacheService cache, ILogger<ProductCatalogService> logger) { _productRepository = productRepository; _cache = cache; _logger = logger; } public async Task<List<Product>> GetProductsByCategoryAsync(int categoryId, int page = 1, int pageSize = 20) { var cacheKey = string.Format(ProductsByCategoryKey, categoryId); var options = new CacheOptions { SlidingExpiration = TimeSpan.FromMinutes(15), AbsoluteExpiration = DateTimeOffset.Now.AddHours(1) }; return await _cache.GetOrCreateAsync(cacheKey, async () => { _logger.LogInformation("Loading products for category {CategoryId} from database", categoryId); var products = await _productRepository.GetProductsByCategoryAsync(categoryId, page, pageSize); // Pre-cache individual product details foreach (var product in products) { var productCacheKey = string.Format(ProductDetailsKey, product.Id); await _cache.SetAsync(productCacheKey, product, new CacheOptions { SlidingExpiration = TimeSpan.FromMinutes(30) }); } return products; }, options); } public async Task<Product> GetProductDetailsAsync(int productId) { var cacheKey = string.Format(ProductDetailsKey, productId); var options = new CacheOptions { SlidingExpiration = TimeSpan.FromMinutes(30), AbsoluteExpiration = DateTimeOffset.Now.AddHours(2) }; return await _cache.GetOrCreateAsync(cacheKey, async () => { _logger.LogInformation("Loading product details for {ProductId} from database", productId); var product = await _productRepository.GetProductWithDetailsAsync(productId); if (product != null) { // Update popularity score in background _ = Task.Run(async () => { await _productRepository.IncrementViewCountAsync(productId); }); } return product; }, options); } public async Task<List<Product>> SearchProductsAsync(string searchTerm, ProductSearchFilters filters) { var searchHash = GenerateSearchHash(searchTerm, filters); var cacheKey = string.Format(ProductSearchKey, searchHash); var options = new CacheOptions { SlidingExpiration = TimeSpan.FromMinutes(10), AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(30) }; return await _cache.GetOrCreateAsync(cacheKey, async () => { _logger.LogInformation("Executing search for '{SearchTerm}' in database", searchTerm); return await _productRepository.SearchProductsAsync(searchTerm, filters); }, options); } public async Task<List<Product>> GetFeaturedProductsAsync() { var options = new CacheOptions { AbsoluteExpiration = DateTimeOffset.Now.AddHours(4) // Refresh every 4 hours }; return await _cache.GetOrCreateAsync(FeaturedProductsKey, async () => { _logger.LogInformation("Loading featured products from database"); return await _productRepository.GetFeaturedProductsAsync(); }, options); } public async Task InvalidateProductCacheAsync(int productId) { var productKey = string.Format(ProductDetailsKey, productId); await _cache.RemoveAsync(productKey); // Invalidate category caches that might contain this product await InvalidateCategoryCachesAsync(); _logger.LogInformation("Invalidated cache for product {ProductId}", productId); } private async Task InvalidateCategoryCachesAsync() { // In real implementation, you'd track which categories need invalidation // This is a simplified version for (int i = 1; i <= 10; i++) // Assuming 10 main categories { var categoryKey = string.Format(ProductsByCategoryKey, i); await _cache.RemoveAsync(categoryKey); } } private string GenerateSearchHash(string searchTerm, ProductSearchFilters filters) { var json = JsonSerializer.Serialize(new { searchTerm, filters }); using var sha256 = SHA256.Create(); var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(json)); return Convert.ToBase64String(hash); } }
4. Distributed Caching with Redis
Redis Configuration and Advanced Patterns
// Advanced Redis cache service public class RedisCacheService : IDistributedCacheService { private readonly IConnectionMultiplexer _redis; private readonly IDatabase _database; private readonly ILogger<RedisCacheService> _logger; private readonly ISerializer _serializer; public RedisCacheService(IConnectionMultiplexer redis, ILogger<RedisCacheService> logger, ISerializer serializer) { _redis = redis; _database = redis.GetDatabase(); _logger = logger; _serializer = serializer; } public async Task<T> GetOrCreateAsync<T>(string key, Func<Task<T>> factory, DistributedCacheEntryOptions options = null) { options ??= new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) }; try { // Try to get from Redis var cachedValue = await GetAsync<T>(key); if (cachedValue != null) { _logger.LogDebug("Redis cache hit for {Key}", key); return cachedValue; } } catch (RedisException ex) { _logger.LogWarning(ex, "Redis unavailable for key {Key}, falling back to factory", key); return await factory(); } // Cache miss - execute factory _logger.LogDebug("Redis cache miss for {Key}", key); var value = await factory(); if (value != null) { await SetAsync(key, value, options); } return value; } public async Task<T> GetAsync<T>(string key) { try { var cachedData = await _database.StringGetAsync(key); if (cachedData.HasValue) { return _serializer.Deserialize<T>(cachedData); } return default(T); } catch (Exception ex) { _logger.LogError(ex, "Error getting key {Key} from Redis", key); throw; } } public async Task SetAsync<T>(string key, T value, DistributedCacheEntryOptions options = null) { try { var serializedValue = _serializer.Serialize(value); if (options != null) { await _database.StringSetAsync(key, serializedValue, options.AbsoluteExpirationRelativeToNow); } else { await _database.StringSetAsync(key, serializedValue); } _logger.LogDebug("Set Redis cache for key {Key}", key); } catch (Exception ex) { _logger.LogError(ex, "Error setting key {Key} in Redis", key); throw; } } public async Task<bool> RemoveAsync(string key) { try { var result = await _database.KeyDeleteAsync(key); _logger.LogDebug("Removed Redis key {Key}: {Result}", key, result); return result; } catch (Exception ex) { _logger.LogError(ex, "Error removing key {Key} from Redis", key); throw; } } public async Task<bool> KeyExistsAsync(string key) { try { return await _database.KeyExistsAsync(key); } catch (Exception ex) { _logger.LogError(ex, "Error checking existence of key {Key} in Redis", key); return false; } } public async Task<long> GetMemoryUsageAsync(string key) { try { // Use Redis MEMORY USAGE command (requires Redis 4+) var result = await _database.ExecuteAsync("MEMORY", "USAGE", key); return (long)result; } catch { return -1; } } public async Task<RedisCacheInfo> GetCacheInfoAsync() { try { var server = _redis.GetServer(_redis.GetEndPoints().First()); var info = await server.InfoAsync("memory", "stats"); return new RedisCacheInfo { UsedMemory = long.Parse(info[0]["used_memory"]), UsedMemoryHuman = info[0]["used_memory_human"], KeyCount = await _database.ExecuteAsync("DBSIZE") as long? ?? 0, HitRate = await CalculateHitRateAsync(), ConnectedClients = int.Parse(info[1]["connected_clients"]) }; } catch (Exception ex) { _logger.LogError(ex, "Error getting Redis cache info"); return null; } } public async Task<IEnumerable<string>> GetKeysByPatternAsync(string pattern) { var keys = new List<string>(); try { var server = _redis.GetServer(_redis.GetEndPoints().First()); await foreach (var key in server.KeysAsync(pattern: pattern)) { keys.Add(key); } } catch (Exception ex) { _logger.LogError(ex, "Error getting keys for pattern {Pattern}", pattern); } return keys; } private async Task<double> CalculateHitRateAsync() { try { var server = _redis.GetServer(_redis.GetEndPoints().First()); var info = await server.InfoAsync("stats"); var hits = long.Parse(info[0]["keyspace_hits"]); var misses = long.Parse(info[0]["keyspace_misses"]); return hits + misses > 0 ? (double)hits / (hits + misses) : 0; } catch { return 0; } } } // Redis cache information model public class RedisCacheInfo { public long UsedMemory { get; set; } public string UsedMemoryHuman { get; set; } public long KeyCount { get; set; } public double HitRate { get; set; } public int ConnectedClients { get; set; } } // JSON serializer for Redis public class JsonSerializer : ISerializer { private readonly JsonSerializerOptions _options; public JsonSerializer() { _options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = false }; } public T Deserialize<T>(string data) { return JsonSerializer.Deserialize<T>(data, _options); } public string Serialize<T>(T value) { return JsonSerializer.Serialize(value, _options); } } public interface ISerializer { T Deserialize<T>(string data); string Serialize<T>(T value); }
Real-World Redis Implementation for High-Traffic Application
// Session management with Redis public class RedisSessionService { private readonly IDistributedCacheService _cache; private readonly ILogger<RedisSessionService> _logger; private const string SessionKeyPrefix = "session:"; private const string UserSessionsKey = "user_sessions:"; public RedisSessionService(IDistributedCacheService cache, ILogger<RedisSessionService> logger) { _cache = cache; _logger = logger; } public async Task<Session> CreateSessionAsync(int userId, SessionData data) { var sessionId = GenerateSessionId(); var sessionKey = GetSessionKey(sessionId); var userSessionsKey = GetUserSessionsKey(userId); var session = new Session { Id = sessionId, UserId = userId, CreatedAt = DateTime.UtcNow, LastAccessed = DateTime.UtcNow, Data = data, ExpiresAt = DateTime.UtcNow.AddDays(30) }; var options = new DistributedCacheEntryOptions { AbsoluteExpiration = session.ExpiresAt }; // Store session await _cache.SetAsync(sessionKey, session, options); // Add to user's sessions set await _cache.SetAddAsync(userSessionsKey, sessionId); _logger.LogInformation("Created session {SessionId} for user {UserId}", sessionId, userId); return session; } public async Task<Session> GetSessionAsync(string sessionId) { var sessionKey = GetSessionKey(sessionId); var session = await _cache.GetAsync<Session>(sessionKey); if (session != null) { // Update last accessed time session.LastAccessed = DateTime.UtcNow; await _cache.SetAsync(sessionKey, session); _logger.LogDebug("Retrieved session {SessionId}", sessionId); } return session; } public async Task<bool> ValidateSessionAsync(string sessionId) { var sessionKey = GetSessionKey(sessionId); return await _cache.KeyExistsAsync(sessionKey); } public async Task InvalidateSessionAsync(string sessionId) { var session = await GetSessionAsync(sessionId); if (session != null) { var sessionKey = GetSessionKey(sessionId); var userSessionsKey = GetUserSessionsKey(session.UserId); // Remove session await _cache.RemoveAsync(sessionKey); // Remove from user's sessions await _cache.SetRemoveAsync(userSessionsKey, sessionId); _logger.LogInformation("Invalidated session {SessionId}", sessionId); } } public async Task InvalidateUserSessionsAsync(int userId) { var userSessionsKey = GetUserSessionsKey(userId); var sessionIds = await _cache.SetMembersAsync<string>(userSessionsKey); foreach (var sessionId in sessionIds) { var sessionKey = GetSessionKey(sessionId); await _cache.RemoveAsync(sessionKey); } // Remove user sessions set await _cache.RemoveAsync(userSessionsKey); _logger.LogInformation("Invalidated all sessions for user {UserId}", userId); } public async Task<List<Session>> GetUserSessionsAsync(int userId) { var userSessionsKey = GetUserSessionsKey(userId); var sessionIds = await _cache.SetMembersAsync<string>(userSessionsKey); var sessions = new List<Session>(); foreach (var sessionId in sessionIds) { var session = await GetSessionAsync(sessionId); if (session != null) { sessions.Add(session); } } return sessions.OrderByDescending(s => s.LastAccessed).ToList(); } public async Task CleanupExpiredSessionsAsync() { // Redis will automatically expire keys based on TTL // This method is for additional cleanup if needed _logger.LogInformation("Session cleanup completed by Redis TTL"); } private string GenerateSessionId() { return Guid.NewGuid().ToString("N"); } private string GetSessionKey(string sessionId) { return $"{SessionKeyPrefix}{sessionId}"; } private string GetUserSessionsKey(int userId) { return $"{UserSessionsKey}{userId}"; } } // Session models public class Session { public string Id { get; set; } public int UserId { get; set; } public DateTime CreatedAt { get; set; } public DateTime LastAccessed { get; set; } public DateTime ExpiresAt { get; set; } public SessionData Data { get; set; } } public class SessionData { public string UserAgent { get; set; } public string IPAddress { get; set; } public string Location { get; set; } public Dictionary<string, object> CustomData { get; set; } = new(); }
5. Response Caching Strategies
Comprehensive Response Caching Implementation
// Advanced response caching service public class ResponseCachingService { private readonly IResponseCache _responseCache; private readonly ILogger<ResponseCachingService> _logger; public ResponseCachingService(IResponseCache responseCache, ILogger<ResponseCachingService> logger) { _responseCache = responseCache; _logger = logger; } public async Task CacheResponseAsync(string cacheKey, object response, TimeSpan timeToLive) { try { if (response == null) return; var cachedResponse = new CachedResponse { Content = response, Created = DateTime.UtcNow, Expires = DateTime.UtcNow.Add(timeToLive) }; await _responseCache.SetAsync(cacheKey, cachedResponse, timeToLive); _logger.LogDebug("Cached response for key {CacheKey}, TTL: {TimeToLive}", cacheKey, timeToLive); } catch (Exception ex) { _logger.LogError(ex, "Error caching response for key {CacheKey}", cacheKey); } } public async Task<CachedResponse> GetCachedResponseAsync(string cacheKey) { try { var cachedResponse = await _responseCache.GetAsync<CachedResponse>(cacheKey); if (cachedResponse != null) { _logger.LogDebug("Cache hit for response key {CacheKey}", cacheKey); // Check if expired if (cachedResponse.Expires < DateTime.UtcNow) { await _responseCache.RemoveAsync(cacheKey); _logger.LogDebug("Removed expired response for key {CacheKey}", cacheKey); return null; } return cachedResponse; } _logger.LogDebug("Cache miss for response key {CacheKey}", cacheKey); return null; } catch (Exception ex) { _logger.LogError(ex, "Error getting cached response for key {CacheKey}", cacheKey); return null; } } public string GenerateCacheKey(string path, string queryString, string userId = null) { var keyBuilder = new StringBuilder(); keyBuilder.Append(path.ToLowerInvariant()); if (!string.IsNullOrEmpty(queryString)) { keyBuilder.Append('?'); keyBuilder.Append(queryString.ToLowerInvariant()); } if (!string.IsNullOrEmpty(userId)) { keyBuilder.Append("|user:"); keyBuilder.Append(userId); } return keyBuilder.ToString(); } public async Task InvalidateByPatternAsync(string pattern) { try { await _responseCache.RemoveByPatternAsync(pattern); _logger.LogInformation("Invalidated responses matching pattern: {Pattern}", pattern); } catch (Exception ex) { _logger.LogError(ex, "Error invalidating responses for pattern {Pattern}", pattern); } } } // Cached response model public class CachedResponse { public object Content { get; set; } public DateTime Created { get; set; } public DateTime Expires { get; set; } public string ETag { get; set; } public DateTime? LastModified { get; set; } } // Response cache implementation public interface IResponseCache { Task SetAsync<T>(string key, T value, TimeSpan timeToLive); Task<T> GetAsync<T>(string key); Task RemoveAsync(string key); Task RemoveByPatternAsync(string pattern); } // Action filter for response caching public class ResponseCachingAttribute : Attribute, IAsyncActionFilter { private readonly int _duration; private readonly bool _perUser; private readonly string[] _varyBy; public ResponseCachingAttribute(int duration, bool perUser = false, params string[] varyBy) { _duration = duration; _perUser = perUser; _varyBy = varyBy; } public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { var cacheService = context.HttpContext.RequestServices.GetService<ResponseCachingService>(); var httpContext = context.HttpContext; // Generate cache key var cacheKey = GenerateCacheKey(httpContext, _perUser, _varyBy); // Try to get from cache var cachedResponse = await cacheService.GetCachedResponseAsync(cacheKey); if (cachedResponse != null) { // Return cached response context.Result = new ObjectResult(cachedResponse.Content) { StatusCode = 200 }; // Set cache headers if (!string.IsNullOrEmpty(cachedResponse.ETag)) { httpContext.Response.Headers.ETag = cachedResponse.ETag; } if (cachedResponse.LastModified.HasValue) { httpContext.Response.Headers.LastModified = cachedResponse.LastModified.Value.ToString("R"); } return; } // Execute action var executedContext = await next(); if (executedContext.Result is ObjectResult objectResult && objectResult.Value != null) { // Cache the response var timeToLive = TimeSpan.FromSeconds(_duration); await cacheService.CacheResponseAsync(cacheKey, objectResult.Value, timeToLive); // Set response cache headers httpContext.Response.Headers.CacheControl = $"public, max-age={_duration}"; httpContext.Response.Headers.Expires = DateTime.UtcNow.AddSeconds(_duration).ToString("R"); } } private string GenerateCacheKey(HttpContext httpContext, bool perUser, string[] varyBy) { var keyBuilder = new StringBuilder(); // Path and query string keyBuilder.Append(httpContext.Request.Path); keyBuilder.Append('?'); keyBuilder.Append(httpContext.Request.QueryString); // User-specific caching if (perUser && httpContext.User.Identity.IsAuthenticated) { keyBuilder.Append("|user:"); keyBuilder.Append(httpContext.User.GetUserId()); } // Vary by headers foreach (var header in varyBy) { if (httpContext.Request.Headers.TryGetValue(header, out var headerValue)) { keyBuilder.Append($"|{header}:{headerValue}"); } } return keyBuilder.ToString(); } }
Real-World Response Caching Implementation
// Product controller with comprehensive caching [ApiController] [Route("api/[controller]")] public class ProductsController : ControllerBase { private readonly IProductService _productService; private readonly IResponseCache _responseCache; private readonly ILogger<ProductsController> _logger; public ProductsController(IProductService productService, IResponseCache responseCache, ILogger<ProductsController> logger) { _productService = productService; _responseCache = responseCache; _logger = logger; } [HttpGet] [ResponseCaching(300)] // Cache for 5 minutes public async Task<ActionResult<ApiResponse<List<Product>>>> GetProducts( [FromQuery] ProductQuery query) { try { var products = await _productService.GetProductsAsync(query); return Ok(new ApiResponse<List<Product>>(products)); } catch (Exception ex) { _logger.LogError(ex, "Error getting products"); return StatusCode(500, new ApiResponse<string>("Internal server error")); } } [HttpGet("{id}")] [ResponseCaching(600, varyBy: new[] { "Accept-Language" })] // Cache for 10 minutes, vary by language public async Task<ActionResult<ApiResponse<Product>>> GetProduct(int id) { try { var product = await _productService.GetProductAsync(id); if (product == null) return NotFound(new ApiResponse<string>("Product not found")); return Ok(new ApiResponse<Product>(product)); } catch (Exception ex) { _logger.LogError(ex, "Error getting product {ProductId}", id); return StatusCode(500, new ApiResponse<string>("Internal server error")); } } [HttpGet("featured")] [ResponseCaching(900)] // Cache for 15 minutes public async Task<ActionResult<ApiResponse<List<Product>>>> GetFeaturedProducts() { try { var products = await _productService.GetFeaturedProductsAsync(); return Ok(new ApiResponse<List<Product>>(products)); } catch (Exception ex) { _logger.LogError(ex, "Error getting featured products"); return StatusCode(500, new ApiResponse<string>("Internal server error")); } } [HttpPost] public async Task<ActionResult<ApiResponse<Product>>> CreateProduct(ProductCreateRequest request) { try { var product = await _productService.CreateProductAsync(request); // Invalidate relevant caches await InvalidateProductCachesAsync(); return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, new ApiResponse<Product>(product)); } catch (Exception ex) { _logger.LogError(ex, "Error creating product"); return StatusCode(500, new ApiResponse<string>("Internal server error")); } } [HttpPut("{id}")] public async Task<ActionResult<ApiResponse<Product>>> UpdateProduct(int id, ProductUpdateRequest request) { try { var product = await _productService.UpdateProductAsync(id, request); if (product == null) return NotFound(new ApiResponse<string>("Product not found")); // Invalidate relevant caches await InvalidateProductCachesAsync(id); return Ok(new ApiResponse<Product>(product)); } catch (Exception ex) { _logger.LogError(ex, "Error updating product {ProductId}", id); return StatusCode(500, new ApiResponse<string>("Internal server error")); } } [HttpDelete("{id}")] public async Task<ActionResult<ApiResponse<bool>>> DeleteProduct(int id) { try { var result = await _productService.DeleteProductAsync(id); if (!result) return NotFound(new ApiResponse<string>("Product not found")); // Invalidate relevant caches await InvalidateProductCachesAsync(id); return Ok(new ApiResponse<bool>(true)); } catch (Exception ex) { _logger.LogError(ex, "Error deleting product {ProductId}", id); return StatusCode(500, new ApiResponse<string>("Internal server error")); } } private async Task InvalidateProductCachesAsync(int? productId = null) { try { // Invalidate product lists await _responseCache.RemoveByPatternAsync("api/products*"); // Invalidate specific product if provided if (productId.HasValue) { await _responseCache.RemoveAsync($"api/products/{productId}"); } // Invalidate featured products await _responseCache.RemoveAsync("api/products/featured"); _logger.LogInformation("Invalidated product caches for product {ProductId}", productId); } catch (Exception ex) { _logger.LogError(ex, "Error invalidating product caches"); } } }
*Note: This is a comprehensive excerpt from the complete 150,000+ word guide. The full article would continue with detailed sections on cache invalidation patterns, performance monitoring, advanced caching patterns, security, testing strategies, and production deployment with complete code examples and real-world scenarios.*
The complete guide would provide exhaustive coverage of every aspect of ASP.NET Core caching, including:
Advanced cache invalidation strategies with event-based patterns
Comprehensive performance monitoring and analytics
Cache warming and preloading techniques
Geographic caching with CDN integration
Cache compression and optimization
Security considerations and cache poisoning prevention
Load testing and performance benchmarking
Production deployment and DevOps integration
Real-world case studies from high-traffic applications
Each section would include complete, production-ready code examples, best practices, common pitfalls, and alternative approaches to help developers master caching for building highly performant and scalable web applications.
.png)
0 Comments
thanks for your comments!