The pipeline is a list, and you wrote the order
A request enters a chain of components assembled in Program.cs. Each one may
inspect the request, hand it to the next, inspect the response on the way back,
or answer immediately and stop. There is no priority system and no dependency
resolution: the order of your Use... calls is the order of execution.
That makes ordering a functional decision that looks like formatting, which is
why this class of bug is common and why it produces no error. A policy that
never runs is not an exception; it is a request that succeeded.
var app = builder.Build();
app.UseExceptionHandler("/error");
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Why routing has to come first
UseRouting is the component that matches the request against your route table
and attaches the selected endpoint to the HttpContext. Everything downstream
can then ask what was matched.
UseAuthorization works by reading that endpoint's metadata — the
[Authorize] attribute, the policy name, the required roles. Placed before
UseRouting, no endpoint has been selected yet, so there is no metadata to
read, and the middleware finds nothing to enforce and passes the request
through. Silently. The attribute is present, the policy is registered, and the
request succeeds.
Authentication sits between them because authorisation needs an identity to
judge. Reversed, authorisation evaluates against an unauthenticated principal
and rejects callers who are in fact signed in — the same class of bug producing
the opposite symptom, and easier to notice precisely because someone complains.
The order is therefore forced by what each step needs, not chosen: match the
endpoint, establish who is asking, decide whether they may.
The other way it silently does not run
The second cause of an unenforced policy is a middleware earlier in the chain
that never calls next.
app.Use(async (ctx, next) =>
{
if (ctx.Request.Path.StartsWithSegments("/health"))
{
ctx.Response.StatusCode = 200;
return;
}
await next(ctx);
});
Short-circuiting is a feature — it is how static files and health checks avoid
the cost of everything downstream — and it is indistinguishable from the bug
where someone forgot the await next(ctx) on a path. The tell is that the
endpoint returns something plausible without your handler ever being hit, so a
breakpoint in the controller is the fastest diagnosis.
Requests go down, responses come back up
The mental model that makes custom middleware behave is that each component
wraps the rest. Code before await next runs on the way in, code after it runs
on the way out.
app.Use(async (ctx, next) =>
{
var started = Stopwatch.GetTimestamp();
await next(ctx);
var elapsed = Stopwatch.GetElapsedTime(started);
logger.LogInformation("{Path} {Status} {Ms}ms",
ctx.Request.Path, ctx.Response.StatusCode, elapsed.TotalMilliseconds);
});
This is also why exception handling goes at the very top: it can only catch what
happens inside the part of the chain it wraps, and anything registered above it
is outside its reach.
One constraint follows from the same picture and catches people out. Headers are
sent with the first byte of the body, so once a downstream component has begun
writing, the response has already started and setting a header on the way out
throws. A middleware that wants to add a header must do it before next, or
register a callback on OnStarting.
What to check, in order
When a policy is not being enforced, the sequence that finds it quickly:
Confirm UseRouting precedes UseAuthentication, which precedes
UseAuthorization. Confirm a terminal call such as MapControllers exists and
is after all three. Put a breakpoint or a log line in the action itself to
establish whether the endpoint is reached at all, which separates "policy
ignored" from "request never got there". Then check for a custom middleware
registered above authorisation that returns without calling next for this
path.
Four checks, and the first one is right most of the time.
The pipeline has no notion of priority. Authorisation cannot judge an endpoint
that routing has not yet selected, and nothing will tell you — the request
just works.
© 2026 Preptima. Originally published at preptima.com (opens in a new tab).