Minimal API Workshop
Module 4

Performance, Resilience & Testing

caching · background work · resilience · health · tests

2:30 – 3:45 PM

By the end you can

Where the time goes

VersionElapsed
Tracking query, entities, mapped in memory~9 ms
AsNoTracking + projection (Module 2)~3 ms
Output cached< 1 ms

Measure first. Caching is what you reach for after the query is already good.

Output caching — cache the response

builder.Services.AddOutputCache(o => o.AddPolicy("Products", p => p
    .Expire(TimeSpan.FromMinutes(5)).SetVaryByQuery("roast","inStock").Tag("products")));
// ...
products.CacheOutput("Products");
await cache.EvictByTagAsync("products", ct);   // when a product changes

Authenticated responses are never cached — that's the right default.

HybridCache — cache the data

builder.Services.AddHybridCache();

var product = await cache.GetOrCreateAsync($"product:{id}",
    async ct => await db.Products.AsNoTracking().FirstOrDefaultAsync(p => p.Id == id, ct),
    tags: ["products"], cancellationToken: ct);

In-process L1 (+ optional L2), with stampede protection. Used for pricing inside a handler.

Move slow work off the request

builder.Services.AddSingleton(Channel.CreateBounded<OrderReadyMessage>(100));
builder.Services.AddHostedService<OrderReadyNotifier>();

// in the status handler, when the order becomes Ready:
await channel.Writer.WriteAsync(new OrderReadyMessage(order.Id, email), ct);

A Channel + BackgroundService. Wrap each message in try/catch — an escape stops the host.

Outbound resilience — one line

builder.Services.AddHttpClient<PaymentClient>(c => c.BaseAddress = new(paymentsUrl))
    .AddStandardResilienceHandler();

// retry (backoff+jitter) · circuit breaker · timeout · rate limiter

The fake payment provider fails ~30% on purpose. Every retry is a span you'll see in Aspire.

Testing the real thing

Test authentication handler

// X-Test-User: "staff"  or  "customer:1"
var claims = new List<Claim> { new(ClaimTypes.Name, spec) };
if (spec == "staff") claims.Add(new(ClaimTypes.Role, "Staff"));
else claims.Add(new("customer_id", spec.Split(':')[1]));
return AuthenticateResult.Success(new(new ClaimsPrincipal(id), Scheme));

Real JWTs aren't needed in tests — the header describes the identity; the policies do the rest.

Lab

Lab 4 · Fast, Resilient & Proven

Ends at checkpoint-4

Module 4 — Production Readiness
1 / 1
Speaker notes

Keyboard

SpaceNext (reveals bullets, then advances)
Back
Home / EndFirst / last
gOverview grid
sSpeaker notes
tLight / dark
fFullscreen
pPrint / save PDF
?This help
EscClose help/overview, or exit to the site

Swipe left/right on touch screens.