Caching Pattern¶
Server-side HybridCache via MiniatR pipeline with tag-based invalidation.
Overview¶
flowchart TB
Q[Query with ICacheableQuery] --> C{HybridCache?}
C -->|Hit| R1[Return Cached]
C -->|Miss| H1[Handler]
H1 --> S[Store with Tags]
S --> R2[Return Fresh]
CMD[Command with ICacheInvalidatingCommand] --> H2[Handler]
H2 --> I[Invalidate by Tags]
I -.->|Clears tagged entries| C Server-Side Caching (HybridCache)¶
Making a Query Cacheable¶
Implement ICacheableQuery with cache key, duration, and tags:
public sealed class GetCategoriesQuery : IRequest<IList<CategoryResponse>>, IRequestWithCurrentGroupId, ICacheableQuery
{
public Guid CurrentGroupId { get; set; }
public string CacheKey => CacheKeys.Categories(CurrentGroupId);
public TimeSpan CacheDuration => CacheDurations.Categories;
public IReadOnlyList<string> Tags => [CacheTags.Group(CurrentGroupId), CacheTags.Categories];
}
Cache Invalidation¶
Commands implement ICacheInvalidatingCommand with tags to invalidate:
public sealed class CreateCategoryCommand : IRequest<CategoryResponse>, IRequestWithCurrentGroupId, ICacheInvalidatingCommand
{
public CategoryRequest Model { get; }
public Guid CurrentGroupId { get; set; }
public IReadOnlyList<string> TagsToInvalidate => [CacheTags.Group(CurrentGroupId), CacheTags.Categories];
}
Constants Structure¶
Cache configuration is split into separate files in Application/Common/Constants/:
| File | Purpose |
|---|---|
CacheKeys.cs | Cache key generators (group-scoped) |
CacheTags.cs | Tag constants for invalidation |
CacheDurations.cs | TimeSpan durations |
CacheConstants.cs | Config (MaxPayloadBytes, MaxKeyLength) |
// CacheKeys.cs
public static string Categories(Guid groupId) => $"unicorn_categories_{groupId}";
public static string MonthlyReport(Guid groupId, int year, int month, ...) => $"unicorn_monthly_{groupId}_{year}_{month}_...";
// CacheTags.cs
public static string Group(Guid groupId) => $"tag:group:{groupId}";
public const string Categories = "tag:categories";
public const string Reports = "tag:reports";
// CacheDurations.cs
public static readonly TimeSpan Categories = TimeSpan.FromHours(1);
public static readonly TimeSpan MonthlyReport = TimeSpan.FromHours(1);
Tag Invalidation Strategy¶
Prefer specific tags over group-wide invalidation:
// Too aggressive - invalidates EVERYTHING for the group
public IReadOnlyList<string> TagsToInvalidate => [CacheTags.Group(GroupId)];
// Better - only invalidates relevant caches
public IReadOnlyList<string> TagsToInvalidate => [CacheTags.Categories];
// When multiple types affected
public IReadOnlyList<string> TagsToInvalidate => [CacheTags.BudgetAccounts, CacheTags.Reports];
When to Cache¶
| Cache | Don't Cache |
|---|---|
| List queries | User-specific mutable data |
| Reference data (categories) | Real-time data |
| Reports/aggregations | Small/fast queries |
| Geocode lookups | - |
HybridCache Benefits¶
- Stampede protection: 100 concurrent requests = 1 DB query
- Tag-based invalidation:
RemoveByTagAsync("reports")clears all report caches - L2 ready: Can add Redis for multi-instance deployments
Related¶
- CQRS — Command/Query pattern
- Architecture — System design