Skip to content

Minimal APIs

Aghogho Bernard edited this page May 12, 2026 · 2 revisions

Minimal APIs

CacheWeave supports ASP.NET Core Minimal APIs via CacheWeaveEndpointFilter, an IEndpointFilter, and the WithCacheWeave() / WithCacheWeaveEvict() extension methods on RouteHandlerBuilder.

Setup

No additional registration is required beyond AddCacheWeave:

builder.Services.AddCacheWeave(options => { ... });

WithCacheWeave()

Chain WithCacheWeave(...) onto any route handler to cache its response:

app.MapGet("/api/products", async (IMediator mediator, [AsParameters] GetProductsQuery query) =>
{
    var result = await mediator.Send(query);
    return Results.Ok(result);
})
.WithCacheWeave("products:list", expirySeconds: 300, includeQueryParams: true);

Overloads

// Explicit key
.WithCacheWeave("products:list", expirySeconds: 300)

// Derived key (resolves to endpoint display name)
.WithCacheWeave()

// Full options
.WithCacheWeave(new CacheWeaveAttribute("products:list")
{
    ExpirySeconds = 300,
    IncludeRouteParams = true,
    IncludeQueryParams = true,
    NoCacheWhen = NoCacheCondition.OnErrorOrEmpty,
    SlidingExpiry = false
})

Route Parameters

Route parameters (path segments like {id}) are automatically included in the cache key by default. Framework route values (controller, action, page, area) are always excluded.

// GET /api/products/42 → key: "products:id=42"
app.MapGet("/api/products/{id}", async (IMediator mediator, Guid id) =>
{
    var product = await mediator.Send(new GetProductByIdQuery(id));
    return product is null ? Results.NotFound() : Results.Ok(product);
})
.WithCacheWeave("products", expirySeconds: 600);

// Exclude specific route params
// GET /api/products/42/draft → key: "products:id=42" (version excluded)
app.MapGet("/api/products/{id}/{version}", async (int id, string version) => ...)
    .WithCacheWeave("products", excludeRouteParams: ["version"]);

// Disable route params entirely
app.MapGet("/api/products/{id}/stats", async (int id) => ...)
    .WithCacheWeave("products:stats", includeRouteParams: false);

// Route params + query params combined
// GET /api/products/42/reviews?page=2 → key: "products:reviews:id=42:page=2"
app.MapGet("/api/products/{id}/reviews", async (int id, int page) => ...)
    .WithCacheWeave("products:reviews");

WithCacheWeaveEvict()

Chain WithCacheWeaveEvict(...) onto mutation routes:

app.MapPost("/api/products", async (IMediator mediator, CreateProductCommand cmd) =>
{
    var id = await mediator.Send(cmd);
    return Results.Created($"/api/products/{id}", id);
})
.WithCacheWeaveEvict(prefix: "products:");

Overloads

// Evict by exact key
.WithCacheWeaveEvict(key: "products:list")

// Evict by prefix
.WithCacheWeaveEvict(prefix: "products:")

// Evict on failure too
.WithCacheWeaveEvict(prefix: "products:", evictOnFailure: true)

Full Example — Products Endpoints

var products = app.MapGroup("/api/products").WithTags("Products");

products.MapGet("/", async (IMediator mediator, [AsParameters] GetProductsQuery query) =>
    Results.Ok(await mediator.Send(query)))
    .WithCacheWeave("products:list", expirySeconds: 300, includeQueryParams: true);

// Route param {id} is automatically included in the key
// GET /api/products/abc-123 → key: "products:detail:id=abc-123"
products.MapGet("/{id:guid}", async (IMediator mediator, Guid id) =>
{
    var product = await mediator.Send(new GetProductByIdQuery(id));
    return product is null ? Results.NotFound() : Results.Ok(product);
})
.WithCacheWeave("products:detail", expirySeconds: 600);

products.MapPost("/", async (IMediator mediator, CreateProductCommand cmd) =>
{
    var id = await mediator.Send(cmd);
    return Results.Created($"/api/products/{id}", id);
})
.WithCacheWeaveEvict(prefix: "products:");

products.MapPut("/{id:guid}", async (IMediator mediator, Guid id, UpdateProductCommand cmd) =>
{
    await mediator.Send(cmd with { Id = id });
    return Results.NoContent();
})
.WithCacheWeaveEvict(prefix: "products:");

products.MapDelete("/{id:guid}", async (IMediator mediator, Guid id) =>
{
    await mediator.Send(new DeleteProductCommand(id));
    return Results.NoContent();
})
.WithCacheWeaveEvict(prefix: "products:");

POST Endpoints with Body Hashing

app.MapPost("/api/products/search", async (IMediator mediator, SearchProductsQuery query) =>
    Results.Ok(await mediator.Send(query)))
    .WithCacheWeave(new CacheWeaveAttribute("products:search")
    {
        HashBody = true,
        HashBodyFields = ["Term", "CategoryId", "Filters"],
        ExpirySeconds = 120
    });

Combining with Other Filters

WithCacheWeave() adds CacheWeaveEndpointFilter to the endpoint's filter pipeline. It composes with other filters in declaration order:

app.MapGet("/api/products", handler)
    .RequireAuthorization()          // auth filter runs first
    .WithCacheWeave("products:list") // cache filter runs second
    .WithOpenApi();

Derived Keys in Minimal APIs

When no key is provided, CacheWeave falls back to ActionDescriptor.DisplayName (the endpoint's display name, typically the route pattern). For predictability, always provide an explicit key in Minimal APIs.

Limitations

  • [CacheWeave] and [CacheWeaveEvict] attributes on lambda handlers are not read — use WithCacheWeave() and WithCacheWeaveEvict() instead.
  • Attribute decoration works only on controller actions and Razor Page handlers.

Clone this wiki locally