Skip to content

Programmatic API

Aghogho Bernard edited this page May 6, 2026 · 1 revision

Programmatic API

ICacheWeaveService provides direct cache access for scenarios where attribute decoration is not possible — service-layer code, background jobs, event handlers, or complex conditional logic.

Interface

public interface ICacheWeaveService
{
    Task<T?> GetOrSetAsync<T>(
        string key,
        Func<CancellationToken, Task<T?>> factory,
        TimeSpan? expiry = null,
        CancellationToken ct = default);

    Task<T?> GetAsync<T>(
        string key,
        CancellationToken ct = default);

    Task SetAsync<T>(
        string key,
        T value,
        TimeSpan? expiry = null,
        CancellationToken ct = default);

    Task InvalidateAsync(
        string key,
        CancellationToken ct = default);

    Task InvalidateByPrefixAsync(
        string prefix,
        CancellationToken ct = default);

    Task InvalidateByPrefixesAsync(
        IEnumerable<string> prefixes,
        CancellationToken ct = default);
}

Registration

ICacheWeaveService is registered automatically by AddCacheWeave. Inject it anywhere in your DI graph:

public class ProductService(ICacheWeaveService cache, AppDbContext db)
{
    ...
}

GetOrSetAsync<T>

The primary method. Returns the cached value if present; otherwise executes the factory, writes the result to the cache, and returns it.

The factory receives the CancellationToken so downstream async calls can be cancelled correctly:

var products = await cache.GetOrSetAsync<List<ProductDto>>(
    key: "products:list:page=1",
    factory: async ct => await db.Products
        .OrderBy(p => p.Name)
        .Take(20)
        .Select(p => new ProductDto(p))
        .ToListAsync(ct),
    expiry: TimeSpan.FromMinutes(5));

Skipping the Cache Write

Return null from the factory to skip writing to the cache:

var result = await cache.GetOrSetAsync<PagedResult<ProductDto>>(
    key: "products:list:page=1",
    factory: async ct =>
    {
        var items = await db.Products.Take(20).ToListAsync(ct);
        // Don't cache empty pages
        return items.Count > 0 ? new PagedResult<ProductDto>(items) : null;
    },
    expiry: TimeSpan.FromMinutes(5));

Stampede Protection

GetOrSetAsync is protected by ICacheStampedeProtector. Concurrent calls with the same key will only execute the factory once. See Stampede Protection.


GetAsync<T>

Read-only cache lookup. Returns null on a miss.

var cached = await cache.GetAsync<ProductDto>("products:detail:abc123");
if (cached is null)
{
    // Cache miss — fetch from DB
}

SetAsync<T>

Write a value directly to the cache, bypassing the factory pattern.

await cache.SetAsync(
    key: "products:detail:abc123",
    value: productDto,
    expiry: TimeSpan.FromMinutes(10));

Useful after a write operation when you want to pre-populate the cache with the known new value rather than waiting for the next read to re-fetch it.

public async Task<ProductDto> CreateAsync(CreateProductCommand cmd)
{
    var product = await db.Products.AddAsync(new Product(cmd));
    await db.SaveChangesAsync();

    var dto = new ProductDto(product.Entity);

    // Pre-populate the detail cache
    await cache.SetAsync($"products:detail:{dto.Id}", dto, TimeSpan.FromMinutes(10));

    // Invalidate the list cache
    await cache.InvalidateByPrefixAsync("products:list:");

    return dto;
}

InvalidateAsync

Remove a single cache entry by exact key.

await cache.InvalidateAsync("products:list:page=1");

InvalidateByPrefixAsync

Remove all cache entries whose key starts with the given prefix.

await cache.InvalidateByPrefixAsync("products:");

Requires a provider that supports prefix removal (Redis, InMemory, SQLite). Throws NotSupportedException on DynamoDB, NCache, Memcached, and FASTER KV.


InvalidateByPrefixesAsync

Remove all cache entries matching any of the given prefixes in a single call. Equivalent to calling InvalidateByPrefixAsync in a loop, but expressed as a single intent:

await cache.InvalidateByPrefixesAsync([
    "products:list:",
    "products:search:",
    "dashboard:stats:"
]);

Useful when a single mutation invalidates entries across multiple key namespaces.


Key Versioning

Keys passed to ICacheWeaveService are used verbatim — the global KeyVersion and KeySeparator from CacheWeaveOptions are not automatically applied. If you want version-aware keys, include the version manually:

var version = options.Value.KeyVersion ?? string.Empty;
var key = string.IsNullOrEmpty(version)
    ? "products:list"
    : $"products:list:{version}";

var result = await cache.GetOrSetAsync<List<ProductDto>>(key, factory, expiry);

Or inject ICacheKeyBuilder to build keys consistently:

public class ProductService(
    ICacheWeaveService cache,
    ICacheKeyBuilder keyBuilder,
    IOptions<CacheWeaveOptions> options)
{
    public async Task<List<ProductDto>> GetAllAsync()
    {
        // Build a key the same way the filter would
        var key = keyBuilder.Build("products:list", options.Value, queryParams: null, contextSegment: null);
        return await cache.GetOrSetAsync<List<ProductDto>>(key, factory, TimeSpan.FromMinutes(5));
    }
}

Full Example — Service Layer

public class MaterialCategoryService(
    ICacheWeaveService cache,
    AppDbContext db,
    IOptions<CacheWeaveOptions> opts)
{
    private string Key(string suffix) =>
        $"material-categories:{opts.Value.KeyVersion}:{suffix}";

    public Task<List<MaterialCategoryDto>> GetAllAsync(CancellationToken ct = default)
        => cache.GetOrSetAsync(
            Key("list"),
            factory: ct => db.MaterialCategories
                .OrderBy(c => c.Name)
                .Select(c => new MaterialCategoryDto(c))
                .ToListAsync(ct),
            expiry: TimeSpan.FromMinutes(10),
            ct: ct);

    public async Task<MaterialCategoryDto> CreateAsync(
        CreateMaterialCategoryCommand cmd,
        CancellationToken ct = default)
    {
        var entity = new MaterialCategory(cmd);
        db.MaterialCategories.Add(entity);
        await db.SaveChangesAsync(ct);

        var dto = new MaterialCategoryDto(entity);
        await cache.SetAsync(Key($"detail:{dto.Id}"), dto, TimeSpan.FromMinutes(10), ct);
        await cache.InvalidateByPrefixAsync(Key("list"), ct);

        return dto;
    }

    public async Task DeleteAsync(Guid id, CancellationToken ct = default)
    {
        await db.MaterialCategories.Where(c => c.Id == id).ExecuteDeleteAsync(ct);
        await cache.InvalidateAsync(Key($"detail:{id}"), ct);
        await cache.InvalidateByPrefixAsync(Key("list"), ct);
    }
}

Clone this wiki locally