Smart caching with fallback strategies, cache invalidation, key collision prevention, and cache observability.
$ dotnet add package DotNetSmartCacheSmart caching with fallback strategies, cache invalidation, key collision prevention, and cache observability.
dotnet add package DotNetSmartCache
Problem: Application crashes when cache (Redis) is unavailable.
using DotNetSmartCache;
using Microsoft.Extensions.Caching.Memory;
// ✅ GOOD: Cache with automatic fallback
var cache = new SmartCache<string, User>(
memoryCache,
async key => await userService.GetUserAsync(key), // Value factory
new SmartCacheOptions<string, User>
{
DefaultExpiration = TimeSpan.FromMinutes(5),
FallbackOnError = true, // Fallback to value factory if cache fails
KeyPrefix = "users" // Prevent key collisions
},
logger
);
// Get or set - automatically falls back if cache fails
var user = await cache.GetOrSetAsync("user-123");
Problem: Cache not invalidated when data changes, serving stale data.
public class UserService
{
private readonly SmartCache<string, User> _cache;
public async Task<User> GetUserAsync(string userId)
{
// Get from cache or fetch
return await _cache.GetOrSetAsync(userId);
}
public async Task UpdateUserAsync(string userId, User user)
{
await _repository.UpdateAsync(user);
// ✅ GOOD: Invalidate cache after update
_cache.Invalidate(userId);
}
public async Task DeleteUserAsync(string userId)
{
await _repository.DeleteAsync(userId);
// ✅ GOOD: Invalidate cache after delete
_cache.Invalidate(userId);
}
}
Problem: No visibility into cache performance (hit rate, misses, etc.).
// ✅ GOOD: Get cache statistics
var stats = cache.GetStatistics();
logger.LogInformation(
"Cache Stats - Total: {Total}, Hits: {Hits}, Misses: {Misses}, Hit Rate: {HitRate:P2}",
stats.TotalEntries,
stats.Hits,
stats.Misses,
stats.HitRate
);
// Use statistics for monitoring
if (stats.HitRate < 0.5)
{
logger.LogWarning("Cache hit rate is low: {HitRate:P2}", stats.HitRate);
}
Problem: Cache keys from different types collide (e.g., "123" for both User and Order).
// ✅ GOOD: Use key prefix to prevent collisions
var userCache = new SmartCache<string, User>(
memoryCache,
async key => await userService.GetUserAsync(key),
new SmartCacheOptions<string, User>
{
KeyPrefix = "users" // Keys become "users:user-123"
}
);
var orderCache = new SmartCache<string, Order>(
memoryCache,
async key => await orderService.GetOrderAsync(key),
new SmartCacheOptions<string, Order>
{
KeyPrefix = "orders" // Keys become "orders:order-123"
}
);
Problem: Need different fallback strategy when cache fails.
// ✅ GOOD: Custom fallback when cache fails
var cache = new SmartCache<string, User>(
memoryCache,
async key => await userService.GetUserFromDatabaseAsync(key), // Primary
new SmartCacheOptions<string, User>
{
FallbackOnError = true,
FallbackValueFactory = async key =>
{
// Fallback to secondary source if primary fails
return await userService.GetUserFromBackupAsync(key);
}
}
);
public class ProductService
{
private readonly SmartCache<int, Product> _productCache;
private readonly SmartCache<string, List<Product>> _categoryCache;
public ProductService(
IMemoryCache memoryCache,
IProductRepository repository,
ILogger<ProductService> logger)
{
_productCache = new SmartCache<int, Product>(
memoryCache,
async id => await repository.GetByIdAsync(id),
new SmartCacheOptions<int, Product>
{
DefaultExpiration = TimeSpan.FromMinutes(10),
SlidingExpiration = TimeSpan.FromMinutes(5),
FallbackOnError = true,
KeyPrefix = "products"
},
logger
);
_categoryCache = new SmartCache<string, List<Product>>(
memoryCache,
async category => await repository.GetByCategoryAsync(category),
new SmartCacheOptions<string, List<Product>>
{
DefaultExpiration = TimeSpan.FromMinutes(15),
KeyPrefix = "categories"
},
logger
);
}
public async Task<Product> GetProductAsync(int productId)
{
// Automatically caches and falls back if cache fails
return await _productCache.GetOrSetAsync(productId);
}
public async Task<List<Product>> GetProductsByCategoryAsync(string category)
{
return await _categoryCache.GetOrSetAsync(category);
}
public async Task UpdateProductAsync(Product product)
{
await _repository.UpdateAsync(product);
// Invalidate product cache
_productCache.Invalidate(product.Id);
// Invalidate category cache (product category might have changed)
_categoryCache.Invalidate(product.Category);
}
public CacheStatistics GetCacheStatistics()
{
return new CacheStatistics
{
ProductCache = _productCache.GetStatistics(),
CategoryCache = _categoryCache.GetStatistics()
};
}
}
// Usage in controller
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly ProductService _productService;
[HttpGet("{id}")]
public async Task<ActionResult<Product>> GetProduct(int id)
{
var product = await _productService.GetProductAsync(id);
return Ok(product);
}
[HttpPut("{id}")]
public async Task<ActionResult> UpdateProduct(int id, Product product)
{
await _productService.UpdateProductAsync(product);
return NoContent();
}
[HttpGet("stats")]
public ActionResult<CacheStatistics> GetCacheStats()
{
return Ok(_productService.GetCacheStatistics());
}
}
// Invalidate single key
cache.Invalidate("user-123");
// Invalidate all (use sparingly)
cache.InvalidateAll();
// Invalidate related caches
public async Task UpdateUserAsync(User user)
{
await _repository.UpdateAsync(user);
// Invalidate user cache
_userCache.Invalidate(user.Id);
// Invalidate user's orders cache
_userOrdersCache.Invalidate(user.Id);
// Invalidate search cache (user name might have changed)
_userSearchCache.InvalidateAll();
}
SmartCache<TKey, TValue> - Smart cache with fallback and invalidationSmartCacheOptions<TKey, TValue> - Options for cache configurationGetOrSetAsync() - Get from cache or set using value factoryInvalidate() - Invalidate specific cache keyInvalidateAll() - Invalidate all cache entriesGetStatistics() - Get cache performance statisticsCacheStatistics - Cache statistics (hits, misses, hit rate)