Put [Cache] on an interface and a source generator writes the caching proxy for you: no lookup boilerplate, no reflection, and a cache hit that allocates nothing.
- 0 B per cache hit. A
ValueTask<T>hit on the defaultMemoryCacheon .NET 9 and later allocates nothing, and a test fails the build if it ever does. The few cases that still allocate are listed. - About 4× faster than FusionCache on an in-process cache hit, measured in the same run.
- Works under NativeAOT and trimming. The proxy is plain generated code, published and run as a NativeAOT app in CI.
- Mistakes surface at compile time. Unstable key parameters, a missing
HybridCachepackage or a missing AOT serializer are compiler diagnostics, not production surprises.
dotnet add package ZeroAlloc.Cacheusing Microsoft.Extensions.DependencyInjection;
using ZeroAlloc.Cache;
var services = new ServiceCollection();
services.AddProductCatalogCache<ProductCatalog>(); // generated: registers the caching proxy
var catalog = services.BuildServiceProvider().GetRequiredService<IProductCatalog>();
Console.WriteLine(await catalog.GetNameAsync(42, CancellationToken.None)); // miss: calls ProductCatalog
Console.WriteLine(await catalog.GetNameAsync(42, CancellationToken.None)); // hit: no call, no allocation
[Cache(TtlMs = 60_000)]
public interface IProductCatalog
{
ValueTask<string> GetNameAsync(int id, CancellationToken ct);
}
public sealed class ProductCatalog : IProductCatalog
{
public ValueTask<string> GetNameAsync(int id, CancellationToken ct)
{
Console.WriteLine($"Loading product {id}...");
return ValueTask.FromResult($"Product {id}");
}
}Loading product 42... prints once. Callers keep injecting IProductCatalog; caching is invisible to them.
- Transparent caching for any
Task<T>orValueTask<T>method, configured once on the interface and overridden per method. - Generated cache keys from the method's parameters, with
CancellationTokenleft out. The key text is predictable, so you can evict an entry by it. - Absolute or sliding expiry, and
MaxEntriesto bound memory on high-cardinality methods. IMemoryCacheby default, withHybridCacheas an L1 + L2 distributed tier per method:UseHybridCache = true.- One-line DI registration through a generated
Add{Service}Cache<TImpl>()extension. - Built-in metrics and tracing on the BCL
MeterandActivitySource, ready for OpenTelemetry: Telemetry. - Compile-time diagnostics ZC0001–ZC0011: the full list.
In-process cache hit. .NET 10.0.13, BenchmarkDotNet v0.15.8, same run. Allocations are exact; speed is a same-run ratio against hand-written IMemoryCache code.
| Library | Ratio | Allocated |
|---|---|---|
Raw IMemoryCache.GetOrCreateAsync |
1.00 | 104 B |
| ZA.Cache proxy | 1.66 | 0 B |
| FusionCache | 6.88 | 88 B |
ZA.Cache is about 4× faster than FusionCache and the only row that allocates nothing. Against hand-written IMemoryCache code it costs about 1.7× the time, for proxy dispatch, key formatting and telemetry, in exchange for no lookup code at any call site. Methodology and the full numbers: docs/performance.md.
- Documentation: cache.zeroalloc.net
- Getting started, including
HybridCacheunder NativeAOT, and the attribute reference - Related packages: ZeroAlloc.Resilience for retry and circuit-breaker proxies, ZeroAlloc.Telemetry for tracing and metrics proxies
- Contributing: issues and pull requests are welcome on GitHub
- License: MIT