Skip to main content
CodeOath
← All posts

.NET Core / Web API71 min total · 19 parts

Building REST APIs with ASP.NET Core: Routing, Middleware, and Dependency Injection

Part 3 of 19 · ~3 min

The Middleware Pipeline

Once Bench needed to actually do something with a request beyond routing it — log how long it took, later on check who was signed in — Dana started stacking middleware in front of MapControllers():

Diagram of a request passing down through middleware layers, then a response passing back up

app.UseHttpsRedirection();
app.UseRouting();
app.UseCors("Kiosk");
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

Kestrel — the built-in web server — is the very first thing any Bench request touches, and from there it works down that list, entry by entry, top to bottom: each one either does its job and calls the next, or answers the request itself right there and stops the chain cold. Whatever eventually comes back retraces those exact same steps on the way out, just bottom to top instead.

Order isn't a suggestion here, and Bench proved that within the first week it had real auth. Theo added [Authorize] to the reservation endpoints and, working from a half-remembered example, registered the pipeline like this:

app.UseAuthorization();   // wrong: runs before Authentication
app.UseAuthentication();
app.MapControllers();

Every reservation request started coming back 401, including ones from members who were very much signed in. The reason is exactly the ordering: UseAuthorization() runs first in that arrangement, so it's deciding whether the request is allowed to do this before UseAuthentication() has had any chance to figure out who's asking — there's no identity attached to the request yet, so authorization has nothing to work with except "unknown," and unknown never passes. Swapping the two lines fixed it instantly, and it's the kind of bug that's genuinely confusing the first time, because nothing in the stack trace points at ordering — it just looks like auth doesn't work at all.

Custom middleware in Bench is just a function of HttpContext and a next delegate — this is the one that ended up timing every request, later feeding the audit trail from the filters chapter:

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    await next(context); // hand off to whatever's next — this call IS the pipeline
    stopwatch.Stop();
    logger.LogInformation("{Path} took {Ms}ms", context.Request.Path, stopwatch.ElapsedMilliseconds);
});

Drop that await next(context) line and this piece of middleware quietly becomes a wall — nothing registered after it ever gets a turn, and unless this exact function writes a response itself, the caller is left waiting on nothing. Strip away the ASP.NET-specific naming and it's a pattern most backend developers have already met under a different name — a relay of handlers, each one free to act and pass the baton onward or stop the relay outright. Middleware Pipelines Compared walks through Express.js and Django solving that exact problem with their own syntax.

Bench leans on three distinct verbs for building this pipeline. Use is what the timing middleware above is doing — able to call next and keep things moving. Run never calls anything downstream at all; it's reserved for whatever genuinely ends a branch. Map/MapWhen splits the pipeline off entirely based on the path or some condition — Bench uses this exactly once, to give the kiosk's health-check endpoint its own tiny sub-pipeline that skips authentication altogether, since a wall-mounted screen has no keyboard to log in with.