Middleware
Middleware has the same shape as ASP.NET Core’s: do work, call next or don’t, do more work.
app.Use(async (ctx, next) =>{ var sw = Stopwatch.StartNew(); await next(ctx); logger.LogInformation("{Path} -> {Status} in {Ms}ms", ctx.Request.Path, ctx.Response.StatusCode, sw.ElapsedMilliseconds);});Not calling next short-circuits everything below it, including routing.
As a class
Section titled “As a class”Once middleware has dependencies, its own tests, or more than a screenful of code, it wants to be a type:
public sealed class ApiKeyMiddleware(IKeyStore keys) : IHttpMiddleware{ public async ValueTask InvokeAsync(HttpContext context, RequestDelegate next) { if (!keys.IsValid(context.Request.Headers.GetFirst("X-Api-Key"))) { context.Response.StatusCode = StatusCodes.Status401Unauthorized; return; }
await next(context); }}Two ways to add it:
builder.Services.AddSingleton<ApiKeyMiddleware>();app.Use<ApiKeyMiddleware>(); // resolved per request, from the request's own scope
app.Use(new ApiKeyMiddleware(store)); // an instance you already haveUse<T>() resolves from the request scope, so a middleware registered Scoped gets the same
instances as everything else handling that request, and one registered Singleton costs a dictionary
lookup. Constructing it any other way would mean reflection, which is the one thing this server does
not do.
Use(IHttpMiddleware) uses the same instance for every request, so it must be thread-safe and
hold no per-request state.
Ordering
Section titled “Ordering”Middleware runs in registration order, wrapping routing and the terminal handler:
Use(A) ─┐ Use(B) ─┐ routing ─┐ UseAfterRouting(C) ─┐ endpoint handlerThe pipeline is composed once, the first time the server starts or serves a connection. Registering
middleware after that throws — and RestartAsync does not recompose it. Routes, by contrast, can
change at any time.
RequestDelegate returns ValueTask because most handlers complete synchronously and should not
allocate a Task to say so.
Use vs UseAfterRouting
Section titled “Use vs UseAfterRouting”UseAfterRouting runs after the router has chosen an endpoint, wrapping only the endpoint’s own
invocation. The difference is ctx.Endpoint: there it is populated, so the middleware can read the
endpoint’s metadata and decide accordingly.
app.UseAfterRouting(async (ctx, next) =>{ if (ctx.Endpoint?.GetMetadata<AuditMetadata>() is { } audit) await auditLog.RecordAsync(audit.Name, ctx.User);
await next(ctx);});That is what authorization needs: [Authorize] is a property of the endpoint, and there is no
endpoint before routing has run. Requests that matched nothing skip this stage entirely and go
straight to the 404 or 405.
The order that matters
Section titled “The order that matters”For an app using most of the built-ins, this is the order that is actually correct:
app.Use<RequestTimingMiddleware>(); // outermost: sees everything, including rejections
app.UseCors(); // a preflight carries no credentials — authenticating it would 401 the // browser's question and the real request would never be sentapp.UseRateLimiter(); // before routing, so a throttled request costs nothing beyond parsingapp.UseIpFilter();
app.UseResponseCompression();
app.UseAuthentication(); // before routing — identity does not depend on the endpointapp.UseAuthorization(); // after routing — what is required is metadata on the endpoint
app.UseSessions();app.UseStaticFiles(); // falls through to routing when no file matchesUseAuthorization() registers itself as after-routing middleware for you; the rest are ordinary
Use.
Writing to the response from middleware
Section titled “Writing to the response from middleware”Headers are flushed on the first body write, and mutating them afterwards throws. Middleware that
wants to add a header around a handler registers a callback instead of setting it after next:
app.Use((ctx, next) =>{ ctx.Response.OnStarting(() => { ctx.Response.Headers["X-Served-By"] = "Shiny"; return ValueTask.CompletedTask; });
return next(ctx);});OnStarting runs immediately before the status line and headers go to the wire — the last chance to
change either. ctx.Response.HasStarted tells you whether that moment has already passed.
Per-request state
Section titled “Per-request state”ctx.Items is a scratch dictionary allocated on first use, for passing state between middleware in
one request. Anything with a lifetime beyond the request belongs in a session
or a singleton service.


