Skip to content

About

Source-generated zero-allocation caching proxy for .NET — annotate an interface with [Cache] and get a fully wired IMemoryCache or HybridCache proxy at compile time

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

283 Commits

Folders and files

Repository files navigation

ZeroAlloc.Cache

NuGet Build codecov License: MIT AOT GitHub Sponsors

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.

Why ZeroAlloc.Cache

  • 0 B per cache hit. A ValueTask<T> hit on the default MemoryCache on .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 HybridCache package or a missing AOT serializer are compiler diagnostics, not production surprises.

Install

dotnet add package ZeroAlloc.Cache

Quick start

using 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.

What you get

  • Transparent caching for any Task<T> or ValueTask<T> method, configured once on the interface and overridden per method.
  • Generated cache keys from the method's parameters, with CancellationToken left out. The key text is predictable, so you can evict an entry by it.
  • Absolute or sliding expiry, and MaxEntries to bound memory on high-cardinality methods.
  • IMemoryCache by default, with HybridCache as 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 Meter and ActivitySource, ready for OpenTelemetry: Telemetry.
  • Compile-time diagnostics ZC0001–ZC0011: the full list.

Performance

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.

Learn more

About

Source-generated zero-allocation caching proxy for .NET — annotate an interface with [Cache] and get a fully wired IMemoryCache or HybridCache proxy at compile time

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages