Middleware order and short-circuiting
Use, Run, Map and UseWhen, what happens when a component does not call next, and why the exception handler goes first.
After this lesson you can
- Put the built-in middleware in a defensible order and explain why
- Predict which components run when one short-circuits
- Choose between Use, Run, Map and UseWhen
Four methods shape a pipeline:
app.Use(...)adds a component that may callnextor not.app.Run(...)adds a terminal component; it has nonextto call.app.Map("/admin", branch => ...)sends requests whose path starts with that segment into a separate pipeline that never rejoins the main one. The matched segment moves fromRequest.PathtoRequest.PathBase.app.UseWhen(predicate, branch => ...)runs extra components when the predicate holds, then rejoins the main pipeline.
Short-circuiting
A component that writes a response and does not call next
short-circuits the request. Nothing deeper runs, but every component that
already ran still executes its code after next and sees the response on
the way out. Authorization answering 401, the static-file middleware
serving site.css, and a rate limiter answering 429 all work this way.
The model below builds a three-component pipeline from plain delegates
and sends an anonymous and a signed-in request through it.
public class Ctx{ public bool Authenticated; public int Status; public List<string> Log = new();} public static class Solution{ public static List<string> Run() { var pipeline = new List<Func<Action<Ctx>, Action<Ctx>>> { next => c => { c.Log.Add("logging in"); next(c); c.Log.Add("logging out " + c.Status); }, next => c => { if (!c.Authenticated) { c.Status = 401; c.Log.Add("auth: 401, no next"); return; } next(c); }, next => c => { c.Log.Add("routing"); next(c); }, }; Action<Ctx> app = c => { c.Status = 200; c.Log.Add("endpoint"); }; for (int i = pipeline.Count - 1; i >= 0; i--) app = pipeline[i](app); var anonymous = new Ctx { Authenticated = false }; app(anonymous); var signedIn = new Ctx { Authenticated = true }; app(signedIn); return anonymous.Log.Concat(new[] { "---" }).Concat(signedIn.Log).ToList(); }}Order is behaviour
A typical Program.cs reads:
app.UseExceptionHandler("/error");
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
Each position has a reason. The exception handler is first because it
can only catch exceptions thrown by components inside it. Static files
come before authorization, so a file it finds is served without any
authorization check; that is intended for public assets and a leak for
anything that is not. Authentication must come before authorization,
because authentication is what turns the request's credentials into
context.User, and authorization only reads it.
The second model puts a try/catch component, standing in for the exception handler, in two positions.
public static class Solution{ static Func<Action, Action> Handler(List<string> log) => next => () => { try { next(); } catch (Exception e) { log.Add("handled: " + e.Message); } }; static Func<Action, Action> Thrower() => next => () => throw new InvalidOperationException("boom"); static string Execute(List<Func<Action, Action>> pipeline, List<string> log) { Action app = () => log.Add("endpoint"); for (int i = pipeline.Count - 1; i >= 0; i--) app = pipeline[i](app); try { app(); return string.Join(" | ", log); } catch (Exception e) { return "escaped to the server: " + e.Message; } } public static List<string> Run() { var first = new List<string>(); var last = new List<string>(); return new List<string> { Execute(new() { Handler(first), Thrower() }, first), Execute(new() { Thrower(), Handler(last) }, last), }; }}An exception travels outward through the components that called next.
A catch registered after the thrower is deeper in the onion, so the
exception never passes through it.
Common mistakes
- Calling
UseAuthorization()beforeUseAuthentication(). When authorization runs,context.Useris still anonymous, so[Authorize]endpoints answer 401 to callers with a perfectly good token (unless the policy names its own authentication schemes, which authorization then runs itself). - Registering the exception handler after other middleware; exceptions from the components before it escape (the second model).
- Using
StartsWithfor a path check in custom middleware./adminthen also matches/administration; compare on a segment boundary asMapdoes. - Choosing
Mapwhen you meantUseWhen. AMapbranch never returns to the main pipeline, so endpoints after it are unreachable for that path.
What interviewers ask
- Order these: authentication, authorization, routing, exception handling, static files. Why? Exception handling outermost; static files early to short-circuit cheaply; routing before auth so the endpoint's metadata is known; authentication before authorization.
- may call ; is terminal; branches on a path prefix and never rejoins.
Try it yourself
2 visible tests · 3 hidden testspipeline lists components in registration order. An entry is either
a plain name such as "Logging", which always calls next, or a guard
shaped "Name=STATUS@/prefix", which short-circuits with STATUS when
the request path equals the prefix or starts with the prefix followed
by / (so /admin guards /admin and /admin/users but not
/administration). A guard whose prefix does not match calls next.
The endpoint sets status 200.
Implement Trace(List<string> pipeline, string path) returning events
in order:
"in:Name"when a component is entered;"short:Name"when a guard short-circuits;"endpoint"when the endpoint runs;"out:Name:STATUS"when a component finishes, with the status the response has at that moment.
Every component that was entered produces an out line, including a
guard that short-circuited.
Trace(["Logging","StaticFiles=200@/static","Auth=401@/admin"], "/admin/users")Trace(["Logging","StaticFiles=200@/static","Auth=401@/admin"], "/static/site.css")
Sign up to run the tests, check the hidden ones and save your progress. Sign up