Caching¶
Verified by tests
SystemCommandsTests — library CI run #31657041675 (2026-08-13)
Whizbang provides distributed caching capabilities with automatic invalidation support through command-based cache clearing.
Overview¶
Caching in Whizbang follows these principles:
- Distributed First: Built for multi-instance deployments
- Command-Based Invalidation: Cache clearing via messages
- Coordinated: Cache clears propagate across all instances
- Type-Safe: Generic cache keys and values
Clear Cache Command¶
The ClearCacheCommand is a system command (in Whizbang.Core.Commands.System) that enables coordinated cache invalidation across distributed instances. System commands are routed via the whizbang.system.commands namespace, and all services using SharedTopicInboxStrategy automatically subscribe to them.
Usage¶
Usage
using Whizbang.Core.Commands.System;
// Clear a specific cache key
var command = new ClearCacheCommand(CacheKey: "product:123");
await dispatcher.SendAsync(command);
// All instances receive command and clear their caches
Command Structure¶
Command Structure
namespace Whizbang.Core.Commands.System;
/// <summary>
/// Command to clear cached data across all services.
/// </summary>
/// <param name="CacheKey">Optional specific cache key to clear. If null, clears all caches.</param>
/// <param name="CacheRegion">Optional cache region/namespace to target.</param>
[PinnedId("db190b57-50ca-4748-9929-0f090dba9e28")]
public record ClearCacheCommand(
string? CacheKey = null,
string? CacheRegion = null
) : ICommand;
Updated
The shipped command takes a single optional CacheKey and an optional CacheRegion. There is no Keys list and no Pattern property - to clear multiple keys, send multiple commands or use a region; pattern semantics (e.g. treating a key like product:* as a wildcard) are up to your receptor implementation.
Patterns¶
Clear a Specific Key¶
Clear a Specific Key
Clear by Region¶
Clear by Region
// Clear all keys in a region
await dispatcher.SendAsync(new ClearCacheCommand(CacheRegion: "ProductCatalog"));
Clear All¶
Clear All
Implementing a Cache Receptor¶
Handle cache clearing in your service. Receptors implementing IReceptor<TMessage, TResponse> are discovered automatically by the source generators:
Implementing a Cache Receptor
using Whizbang.Core;
using Whizbang.Core.Commands.System;
public class CacheClearReceptor : IReceptor<ClearCacheCommand, CacheCleared> {
private readonly ICacheService _cache;
private readonly ILogger<CacheClearReceptor> _logger;
public CacheClearReceptor(ICacheService cache, ILogger<CacheClearReceptor> logger) {
_cache = cache;
_logger = logger;
}
public async ValueTask<CacheCleared> HandleAsync(
ClearCacheCommand message,
CancellationToken ct = default) {
var clearedKeys = 0;
if (!string.IsNullOrEmpty(message.CacheKey)) {
// Clear a specific key
await _cache.RemoveAsync(message.CacheKey, ct);
clearedKeys = 1;
} else if (!string.IsNullOrEmpty(message.CacheRegion)) {
// Clear region
clearedKeys = await _cache.ClearRegionAsync(message.CacheRegion, ct);
} else {
// Clear all
await _cache.ClearAsync(ct);
clearedKeys = -1; // Unknown count
}
_logger.LogInformation(
"Cache cleared: {KeyCount} keys removed",
clearedKeys == -1 ? "all" : clearedKeys
);
return new CacheCleared {
KeysCleared = clearedKeys,
ClearedAt = DateTimeOffset.UtcNow
};
}
}
public record CacheCleared : IEvent {
public int KeysCleared { get; init; }
public DateTimeOffset ClearedAt { get; init; }
}
ICacheService Interface¶
Whizbang does not ship a cache service abstraction - define one in your application and register your own implementation:
ICacheService Interface
// Application-defined abstraction (not part of Whizbang)
public interface ICacheService {
// Get/Set
Task<T?> GetAsync<T>(string key, CancellationToken ct = default);
Task SetAsync<T>(string key, T value, TimeSpan? expiration = null, CancellationToken ct = default);
// Remove
Task RemoveAsync(string key, CancellationToken ct = default);
Task<int> RemoveByPatternAsync(string pattern, CancellationToken ct = default);
// Clear
Task ClearAsync(CancellationToken ct = default);
Task<int> ClearRegionAsync(string region, CancellationToken ct = default);
// Exists
Task<bool> ExistsAsync(string key, CancellationToken ct = default);
}
Distributed Cache Example¶
Using Redis as distributed cache:
Distributed Cache Example
using StackExchange.Redis;
public class RedisCacheService : ICacheService {
private readonly IConnectionMultiplexer _redis;
private readonly ILogger<RedisCacheService> _logger;
public RedisCacheService(IConnectionMultiplexer redis, ILogger<RedisCacheService> logger) {
_redis = redis;
_logger = logger;
}
public async Task<T?> GetAsync<T>(string key, CancellationToken ct = default) {
var db = _redis.GetDatabase();
var value = await db.StringGetAsync(key);
if (!value.HasValue) {
return default;
}
return JsonSerializer.Deserialize<T>(value!);
}
public async Task SetAsync<T>(
string key,
T value,
TimeSpan? expiration = null,
CancellationToken ct = default) {
var db = _redis.GetDatabase();
var serialized = JsonSerializer.Serialize(value);
await db.StringSetAsync(key, serialized, expiration);
}
public async Task RemoveAsync(string key, CancellationToken ct = default) {
var db = _redis.GetDatabase();
await db.KeyDeleteAsync(key);
}
public async Task<int> RemoveByPatternAsync(string pattern, CancellationToken ct = default) {
var server = _redis.GetServer(_redis.GetEndPoints().First());
var keys = server.Keys(pattern: pattern).ToArray();
var db = _redis.GetDatabase();
await db.KeyDeleteAsync(keys);
return keys.Length;
}
public async Task ClearAsync(CancellationToken ct = default) {
var endpoints = _redis.GetEndPoints();
foreach (var endpoint in endpoints) {
var server = _redis.GetServer(endpoint);
await server.FlushDatabaseAsync();
}
}
public async Task<int> ClearRegionAsync(string region, CancellationToken ct = default) {
return await RemoveByPatternAsync($"{region}:*", ct);
}
public async Task<bool> ExistsAsync(string key, CancellationToken ct = default) {
var db = _redis.GetDatabase();
return await db.KeyExistsAsync(key);
}
}
Registration¶
Registration
// Program.cs
using StackExchange.Redis;
var builder = WebApplication.CreateBuilder(args);
// Register Redis
var redisConnection = await ConnectionMultiplexer.ConnectAsync(
builder.Configuration.GetConnectionString("Redis")!
);
builder.Services.AddSingleton<IConnectionMultiplexer>(redisConnection);
// Register cache service
builder.Services.AddSingleton<ICacheService, RedisCacheService>();
// CacheClearReceptor is discovered and registered automatically
// by Whizbang's receptor source generators - no manual registration needed
var app = builder.Build();
Cache Patterns¶
Write-Through Cache¶
Update cache when data changes:
Write-Through Cache
public class UpdateProductReceptor : IReceptor<UpdateProduct, (ProductUpdated, ClearCacheCommand)> {
private readonly IProductRepository _repository;
public async ValueTask<(ProductUpdated, ClearCacheCommand)> HandleAsync(
UpdateProduct message,
CancellationToken ct = default) {
// Update database
await _repository.UpdateAsync(message.ProductId, message.Name, message.Price, ct);
// Return event + cache clear command (auto-cascade)
return (
new ProductUpdated {
ProductId = message.ProductId,
Name = message.Name,
Price = message.Price
},
new ClearCacheCommand(CacheKey: $"product:{message.ProductId}")
);
}
}
Cache-Aside Pattern¶
Cache-Aside Pattern
public class GetProductReceptor : IReceptor<GetProduct, ProductDto> {
private readonly ICacheService _cache;
private readonly IProductLens _lens;
public async ValueTask<ProductDto> HandleAsync(
GetProduct query,
CancellationToken ct = default) {
var cacheKey = $"product:{query.ProductId}";
// Try cache first
var cached = await _cache.GetAsync<ProductDto>(cacheKey, ct);
if (cached != null) {
return cached;
}
// Cache miss - query database
var product = await _lens.GetProductAsync(query.ProductId, ct);
// Store in cache
await _cache.SetAsync(cacheKey, product, TimeSpan.FromMinutes(15), ct);
return product;
}
}
Best Practices¶
DO¶
- Use distributed cache for multi-instance deployments
- Set expiration times to prevent stale data
- Clear cache proactively when data changes
- Use cache keys consistently (e.g.,
{entity}:{id}pattern) - Handle cache misses gracefully with fallback to database
DON'T¶
- Don't cache forever without expiration
- Don't ignore cache clear failures (log and alert)
- Don't cache sensitive data without encryption
- Don't over-cache (cache only frequently accessed data)
- Don't rely on cache alone (always have database fallback)
See Also¶
- Commands - Command fundamentals
- Dispatcher - Message routing
- Receptors - Command handlers
- Auto-Cascade - Tuple return pattern
Version 1.0.0 - Foundation Release