
Bài trước: ASP.NET Core Performance Hacks: Async, Profiling & Optimization Techniques (Part - 26 of 40)
Mục lục
- Cách mạng Caching
- Cơ bản & Kiến trúc Caching
- In-Memory Caching Deep Dive
- Distributed Caching với 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. Cách mạng Caching
Tại sao Caching là "Silver Bullet" cho Performance
Caching biến đổi hiệu năng ứng dụng bằng cách giảm database load, giảm response times, và cải thiện scalability. Trong ứng dụng web hiện đại, caching không chỉ là optimization—đó là cần thiết.
Analogy thực tế: Hãy tưởng tượng một quán cà phê bận rộn. Không có caching, mỗi đơn hàng khách sẽ yêu cầu:
- Đi đến kho nhà cung cấp (database)
- Chọn hạt cà phê (query execution)
- Xay tươi (data processing)
- Pha riêng lẻ (response generation)
Với caching, các đồ uống phổ biến được chuẩn bị sẵn và sẵn sàng phục vụ ngay lập tức!
// Performance impact demonstration
public class ProductServiceWithoutCaching {
private readonly ApplicationDbContext _context;
public async Task<Product> GetProductAsync(int id) {
// Mọi lần gọi đều hit database - CHẬM!
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) {
// Thử cache trước - NHANH!
if (_cache.TryGetValue($"product_{id}", out Product product)) return product;
// Cache miss - lấy từ database
product = await _context.Products
.Include(p => p.Category)
.Include(p => p.Reviews)
.FirstOrDefaultAsync(p => p.Id == id);
// Lưu vào cache cho các request sau
_cache.Set($"product_{id}", product, TimeSpan.FromMinutes(30));
return product;
}
}
Tác động Performance của Caching
| Scenario | Without Caching | With Caching | Improvement |
|---|---|---|---|
| Database Queries | 1000 queries/sec | 50 queries/sec | Giảm 20x |
| Response Time | 200ms | 5ms | Nhanh 40x |
| Server Load | 80% CPU | 20% CPU | Giảm 4x |
| Cost | $1000/tháng | $250/tháng | Tiết kiệm 75% |
2. Cơ bản & Kiến trúc Caching
Các Pattern Kiến trúc Caching
// 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 {
// Thử memory cache trước (nhanh nhất)
if (_memoryCache.TryGetValue(key, out T cachedValue)) {
_logger.LogDebug("Memory cache hit for key: {Key}", key);
return cachedValue;
}
// Thử distributed cache
var distributedValue = await _distributedCache.GetAsync<T>(key);
if (distributedValue != null) {
_logger.LogDebug("Distributed cache hit for key: {Key}", key);
// Populate memory cache cho truy cập nhanh hơn sau
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) {
// Lưu vào cả hai 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);
// Nếu caching fail, fallback về factory method
return await factory();
}
}
public async Task<T> GetAsync<T>(string key) {
// Implementation tương tự như trên nhưng không có 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 cho cache invalidation
public async Task RemoveByPatternAsync(string pattern) {
// Phiên bản đơn giản - implementation thực tế phụ thuộc cache provider
_logger.LogInformation("Removing cache entries matching pattern: {Pattern}", pattern);
// Trong thực tế, bạn dùng Redis keys command hoặc tương tự
// Đây là implementation conceptual
}
}
// 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;
}
Cấu hình Cache trong 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 (luôn available)
builder.Services.AddMemoryCache(options => {
options.SizeLimit = 1024 * 1024 * 100; // 100MB limit
options.CompactionPercentage = 0.25; // Compact khi 25% full
});
// Add distributed cache dựa trên 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 cho direct access
builder.Services.AddSingleton<IConnectionMultiplexer>(sp => ConnectionMultiplexer.Connect(cacheSettings.RedisConnectionString));
} else {
// Fallback to distributed memory cache (cho 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 với 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 - dùng 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 sau khi acquire lock
if (_cache.TryGetValue(key, out cachedValue)) {
UpdateAccessStats(key);
return cachedValue;
}
var value = await factory();
if (value != null) {
var cacheEntryOptions = CreateCacheEntryOptions(options);
// Register callback cho 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, dùng methods chính xác hơn
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 luôn 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 với 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() {
// Trong thực tế, bạn track categories nào cần invalidate
// Phiên bản đơn giản cho demo
for (int i = 1; i <= 10; i++) // Giả sử 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 với Redis
Redis Configuration và 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 {
// Thử lấy từ 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 {
// Dùng Redis MEMORY USAGE command (yêu cầu 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 cho 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 cho High-Traffic Application
// Session management với 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 sẽ tự động expire keys dựa trên TTL
// Method này cho additional cleanup nếu cần
_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 cho 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);
// Thử lấy từ 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 với 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 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 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 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");
}
}
}
Lưu ý: Đây là trích dẫn toàn diện từ hướng dẫn hoàn chỉnh 150,000+ từ. Bài viết đầy đủ sẽ tiếp tục với các phần chi tiết về cache invalidation patterns, performance monitoring, advanced caching patterns, security, testing strategies, và production deployment với code examples hoàn chỉnh và real-world scenarios.
Hướng dẫn hoàn chỉnh sẽ cung cấp bao phủ toàn diện mọi khía cạnh của ASP.NET Core caching, bao gồm:
- Advanced cache invalidation strategies với event-based patterns
- Comprehensive performance monitoring và analytics
- Cache warming và preloading techniques
- Geographic caching với CDN integration
- Cache compression và optimization
- Security considerations và cache poisoning prevention
- Load testing và performance benchmarking
- Production deployment và DevOps integration
- Real-world case studies từ high-traffic applications
Mỗi section sẽ bao gồm complete, production-ready code examples, best practices, common pitfalls, và alternative approaches để giúp developers master caching cho việc xây dựng high-performance và scalable web applications.