Quickstart
A worked example: cache a read, evict it on write, and warm it again afterwards.
1. Register a backend
using ActionCache.Common.Extensions;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddActionCache(options =>
{
options.UseMemoryCache(memory => memory.SizeLimit = 10_000);
});
var app = builder.Build();
app.MapControllers();
app.Run();2. Cache a response
Namespace is required. It is the group the entry belongs to, and the handle eviction and
refresh use later.
[ApiController]
[Route("forecasts")]
public class ForecastsController : ControllerBase
{
[HttpGet]
[ActionCache(Namespace = "Forecasts")]
public IActionResult Get() => Ok(_repository.All());
}The second identical request is served from the cache without the action running.
3. Evict when the data changes
[HttpPost]
[ActionCacheEviction(Namespace = "Forecasts")]
public IActionResult Create(Forecast forecast) => Ok(_repository.Add(forecast));Eviction runs after a successful response and removes every entry in the namespace — you never name a key.
4. Or refresh instead of evicting
Eviction leaves the next reader to pay for a cold cache. Refresh re-populates the namespace instead, by replaying the request recorded on each entry:
[HttpPost]
[ActionCacheRefresh(Namespace = "Forecasts")]
public IActionResult Create(Forecast forecast) => Ok(_repository.Add(forecast));Minimal APIs
Endpoints use builder extensions rather than attributes:
using ActionCache.EndpointFilters.Extensions;
app.MapGet("/forecasts", () => repository.All())
.WithActionCache("Forecasts");
app.MapDelete("/forecasts/{id}", (int id) => repository.Remove(id))
.WithActionCacheEviction("Forecasts");
app.MapPost("/forecasts", (Forecast forecast) => repository.Add(forecast))
.WithActionCacheRefresh("Forecasts");WithActionCache accepts the same per-endpoint settings as [ActionCache] — expiration,
vary-by and SingleFlight — through a configure delegate:
app.MapGet("/forecasts", () => repository.All())
.WithActionCache("Forecasts", options =>
{
options.AbsoluteExpiration = TimeSpan.FromMinutes(5);
options.VaryByQuery = "page,size";
});Omit the delegate and the endpoint takes the defaults, plus whatever UseEntryOptions
configures globally. See the attributes reference.
What you get without asking
- Per-user keys. On an
[Authorize]endpoint the caller’s identity joins the key, so two users cannot be served each other’s response. See Vary-by. - Stampede protection. Concurrent misses for one key are coalesced and the action runs once. See Stampede protection.
- Fail-open. A backend that throws degrades to a miss and the request still succeeds. See Resilience.