Pragmatic.Caching
Source-generated caching for .NET 10: typed keys, tag-based invalidation, category routing, and HybridCache integration.
The Problem
Section titled “The Problem”Caching in .NET means scattered magic strings, manual serialization, hardcoded TTLs, and ad-hoc invalidation:
var key = $"product:{id}"; // magic string: rename and it breaksvar cached = await cache.GetStringAsync(key, ct);if (cached is not null) return JsonSerializer.Deserialize<ProductDto>(cached);var dto = ProductDto.FromEntity(await repo.GetByIdAsync(id, ct));await cache.SetStringAsync(key, JsonSerializer.Serialize(dto), new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) }, ct);Miss one key in the invalidation handler and you serve stale data; share a backend across subsystems and you get key collisions.
The Solution
Section titled “The Solution”Declare what to cache and when to invalidate; the generator produces the key construction, options, and invalidation at compile time.
[Query<Product, ProductDto>(Single = true)][Cacheable(Duration = "5m", Tags = ["products", "product:{ProductId}"])]public partial class GetProductQuery { public required Guid ProductId { get; init; } }
// A mutation invalidates by tag: no key-pattern duplication[Mutation(Mode = MutationMode.Update)][InvalidatesCache("products")]public partial class UpdateProductMutation : Mutation<Product> { /* ... */ }Typed keys (no magic strings), tag-based invalidation, category routing to isolate subsystems (permissions, rate limiting, output cache) on a shared backend, and HybridCache (in-memory + distributed) integration.
Distributed atomic counters (Redis)
Section titled “Distributed atomic counters (Redis)”ICacheStack.IncrementAsync is atomic per process with the default HybridCacheStack: enough for
single-instance rate limiting, not for a multi-node deployment. The Pragmatic.Caching.Redis package
routes counters to Redis (one atomic INCRBY+PEXPIRE Lua script), leaving every other cache
operation on the existing backend:
services.AddPragmaticCaching();services.AddRedisAtomicCounters("localhost:6379"); // or pre-register IConnectionMultiplexerWith this in place the endpoints’ [RateLimit] limiter enforces limits strictly across instances.
Redis failures propagate (no silent per-instance fallback); the rate limiter fails closed.
Installation
Section titled “Installation”dotnet add package Pragmatic.Cachingdotnet add package Pragmatic.SourceGenerator # generates keys + invalidationStatus
Section titled “Status”Functional within 1.0.0-alpha: [Cacheable]/[InvalidatesCache], typed keys, tag invalidation,
category routing, and the HybridCache integration. See the roadmap.
| Concepts | Typed keys, tags, invalidation model, HybridCache | | Getting Started | Cache a query, invalidate on a mutation | | Categories | Category routing to isolate subsystems on a shared backend | | Common Mistakes | The most frequent caching pitfalls | | Troubleshooting | Problem/solution guide with diagnostics |
Requirements
Section titled “Requirements”- .NET 10.0+
Pragmatic.SourceGeneratoranalyzer
License
Section titled “License”Part of the Pragmatic.Design ecosystem. See Licensing. Pragmatic.Caching is MIT-licensed.
