Skip to content

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.

Next