![caching]()
Previous article: ASP.NET Core Performance Hacks: Async, Profiling & Optimization Techniques (Part - 26 of 40)
Table of Contents
The Caching Revolution
Caching Fundamentals & Architecture
In-Memory Caching Deep Dive
Distributed Caching with Redis
Response Caching Strategies
Real-World E-Commerce Caching
Cache Invalidation Patterns
Performance Monitoring & Analytics
Advanced Caching Patterns
Security & Best Practices
Testing Caching Strategies
Production Deployment
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 demonstrationpublic 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 interfacepublic 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 providerspublic 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 modelpublic 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 setupvar 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 configurationvar 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 policiespublic 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 classespublic 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 cachingpublic 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 servicepublic 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 modelpublic 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 Redispublic 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 Redispublic 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 modelspublic 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 servicepublic 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 modelpublic 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 implementationpublic 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 cachingpublic 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.