Namespaced response caching for ASP.NET Core
Cache a controller action or a Minimal API endpoint with one attribute. Evict and refresh whole namespaces without knowing a single key.
[HttpGet("forecasts")]
[ActionCache(Namespace = "Forecasts")]
public IActionResult Get() => Ok(_repository.All());
[HttpPost("forecasts")]
[ActionCacheEviction(Namespace = "Forecasts")]
public IActionResult Create(Forecast forecast) => Ok(_repository.Add(forecast));Cache a read. Drop the whole namespace on write. No keys to track, and nothing else to wire up.
Install
One package per backend, so you only take the dependencies you use. Every backend package brings the core with it.
Memory
dotnet add package ActionCache
In-process. No infrastructure, and the natural first layer of a chain.
Redis
dotnet add package ActionCache.Redis
The default distributed choice. Atomic through Lua, and the preferred distributed lock.
SQL Server
dotnet add package ActionCache.SqlServer
For a stack that already runs one. Locks with
sp_getapplock.
Azure Cosmos DB
dotnet add package ActionCache.AzureCosmos
TTL-backed documents, with the database and container created on first use.Three attributes
Everything the library does is reached through one of these. Each takes a namespace, which is the group an entry belongs to.
[ActionCache]
Cache the response. Varies by the authenticated user automatically, so one caller is never served another's data.
[ActionCacheEviction]
Drop every entry in a namespace after a successful write. You never name a key.
[ActionCacheRefresh]
Replay the request recorded on each entry, so the cache is left warm rather than empty.What you get
Each of these has a page in the documentation.
Namespace eviction
Entries are grouped under a namespace you name, and a namespace can embed route tokens —
Account:{id} gives every account its own group to evict or refresh.
Per-user by default
On an authenticated endpoint the caller's identity joins the key without being asked for. Two users on one [Authorize] action cannot collide.
Refresh by replay
Refresh re-issues the recorded request against the real endpoint, in its own DI scope — model binding, filters and result execution all run normally.
Fails open
A backend that throws degrades to a miss and the request still succeeds. Opt in to fail-closed, and set a timeout to bound one that hangs instead.
Stampede protection
Concurrent misses for one key are coalesced so the action runs once. In-process by default; opt in to a lock over Redis or SQL Server.
Layered backends
Register several and they chain. A hit in a deeper layer is promoted into the faster one, so Memory + Redis pays the round-trip once.
Hashed keys
SHA-256 over route values, arguments and vary-by values. Nothing needs to reverse a key, so nothing readable is left in the store.
OpenTelemetry
A meter and an activity source, inert until something subscribes. Hit ratio, backend latency by outcome, evictions and coalesced requests.
MVC and Minimal APIs
The same model covers controller actions and endpoints alike, with the right filter chosen at runtime from how the app is built.Choosing a backend
They are interchangeable, and they compose — see layered backends.
| Memory | Redis | SQL Server | Cosmos DB | |
|---|---|---|---|---|
| Package | ActionCache | ActionCache.Redis | ActionCache.SqlServer | ActionCache.AzureCosmos |
| Shared across instances | No | Yes | Yes | Yes |
| Locking | SemaphoreSlim | atomic Lua | sp_getapplock | atomic |
| Distributed single flight | — | Yes | Yes | — |
| Expiry | IMemoryCache | TTL + keyspace events | SqlServerCache | container TTL |
Targets .NET 8 and .NET 10 · Read the documentation