Skip to content

Generated Code

The CacheStore generator produces standard .NET interfaces and implementations that work with plain async/await — no effects required. For the optional Eff<RT, T> composition layer, see Effects Composition.

Generated Cache Interface

For each [Cached] entity, the generator produces a typed cache interface:

public interface IProductCache
{
    Task<Product?> GetAsync(ProductId id, CancellationToken ct = default);
    Task SetAsync(ProductId id, Product value, TimeSpan? expiration = null, CancellationToken ct = default);
    Task RemoveAsync(ProductId id, CancellationToken ct = default);
}

The interface uses the entity's [TypedId] property as the key parameter, so cache operations are fully typed — no raw string keys needed.

ICacheStore-Backed Implementation

A default implementation is generated for every entity that delegates to ICacheStore with automatic key management:

public sealed class ProductCache(ICacheStore cache) : IProductCache
{
    private const string KeyPrefix = "product";

    public Task<Product?> GetAsync(ProductId id, CancellationToken ct = default)
        => cache.GetAsync<Product>($"{KeyPrefix}:{id}", ct);

    public Task SetAsync(ProductId id, Product value, TimeSpan? expiration = null, CancellationToken ct = default)
        => cache.SetAsync($"{KeyPrefix}:{id}", value, expiration, ct);

    public Task RemoveAsync(ProductId id, CancellationToken ct = default)
        => cache.RemoveAsync($"{KeyPrefix}:{id}", ct);
}

This means all generated entity caches share the same underlying ICacheStore — configure Redis once, all caches benefit.

DI Registration

The generator creates a registration extension method:

// Generated
public static IServiceCollection AddAppCache(this IServiceCollection services)
{
    services.AddDefaultCacheStore(); // ICacheStore → DistributedCacheStore → IDistributedCache
    services.TryAddSingleton<IProductCache, ProductCache>();
    services.TryAddSingleton<IOrderCache, OrderCache>();
    return services;
}

AddDefaultCacheStore() registers ICacheStore backed by IDistributedCache (in-memory fallback if none registered). TryAddSingleton means implementations registered first take priority.

Injecting and Using Directly

Use the generated interface with standard constructor injection:

public class ProductService(IProductCache cache, IProductStore store)
{
    public async Task<Product?> GetAsync(ProductId id, CancellationToken ct)
    {
        var cached = await cache.GetAsync(id, ct);
        if (cached is not null) return cached;

        var product = await store.GetByIdAsync(id, ct);
        if (product is not null)
            await cache.SetAsync(id, product, TimeSpan.FromMinutes(10), ct);

        return product;
    }

    public async Task InvalidateAsync(ProductId id, CancellationToken ct)
        => await cache.RemoveAsync(id, ct);
}

Swapping the Cache Backend

Register your preferred IDistributedCache before calling AddAppCache():

// Redis via Aspire
builder.AddRedisDistributedCache("cache");

// Or register directly
services.AddStackExchangeRedisCache(options =>
    options.Configuration = "localhost:6379");

// AddAppCache() won't override your IDistributedCache — all entity caches use Redis
services.AddAppCache();

You can also override ICacheStore entirely for custom backends:

services.AddSingleton<ICacheStore, MyCustomCacheStore>();
services.AddAppCache(); // Generated caches delegate to your custom store