Introduction
Immediate.Cache generates in-memory caches for Immediate.Handlers handlers, with request coalescing, invalidation and read-modify-write built in.
Immediate.Cache caches the responses of Immediate.Handlers handlers. You write a small class, mark it with [CacheFor<THandler>], and describe how a request
becomes a cache key; a source generator makes that class derive from ApplicationCache<TRequest, TResponse> and wires up its constructor and DI registration at compile
time.
Beyond a plain IMemoryCache lookup, the base class gives you request coalescing (simultaneous
callers for the same key share one handler execution), imperative SetValue/RemoveValue invalidation, and an optimistic read-modify-write helper.
Immediate.Cache stores everything in the IMemoryCache registered in your container. There is no IDistributedCache and no HybridCache support, and no extension point to substitute one — the
store is a constructor parameter of ApplicationCache<,> typed as IMemoryCache. Every process
in a multi-instance deployment therefore keeps its own copy, and RemoveValue only invalidates the
instance it runs on.
Installation
dotnet add package Immediate.Cache The package ships the runtime types, the source generator and the analyzers together. It targets net8.0, net9.0 and net10.0.
- Immediate.Handlers is required. Immediate.Cache only wraps
[Handler]classes, and its runtime types referenceIHandler<TRequest, TResponse>. It is pulled in transitively, but you need it in the project that declares your handlers regardless. Microsoft.Extensions.Caching.Memoryis required to callAddMemoryCache(). Immediate.Cache only referencesMicrosoft.Extensions.Caching.Abstractions, so a non-ASP.NET Core project must add the memory-cache package itself. ASP.NET Core projects already have it via the shared framework.
A quick look
[Handler]
public sealed partial class GetUser
{
public sealed record Query(int UserId);
public sealed record Response(int UserId, string Name);
private async ValueTask<Response> HandleAsync(
Query query,
UserRepository repository,
CancellationToken token
) => await repository.GetUser(query.UserId, token);
}
[CacheFor<GetUser>]
public sealed partial class GetUserCache
{
protected override string TransformKey(GetUser.Query request) =>
$"GetUser({request.UserId})";
}Register the generated cache alongside your handlers, then inject GetUserCache and call it:
builder.Services.AddMemoryCache();
builder.Services.AddWebHandlers();
builder.Services.AddWebCaches();var response = await cache.GetValue(new GetUser.Query(42), token); Where to go next
Declare a cache class, satisfy the requirements on the target handler, and register it.
GetValue, SetValue, RemoveValue and TransformValue, and the concurrency rules behind them.
Override GetCacheEntryOptions to change the five-minute sliding expiration default.
The service-collection recipe used by the package's own functional tests.
Every public and protected member of ApplicationCache, Owned and OwnedScope.
Why caches are Singletons, and how Owned resolves the scoped-handler tension.
IC0001–IC0003, the CA2000 suppressor, and troubleshooting compile failures.