Middleware and pipeline order
Skill Sarmkadan/dotnet-senior-skills/skills/middleware-and-pipeline-order
Review ASP.NET Core pipeline configuration - middleware ordering, auth placement, exception handling position, CORS, short-circuiting, and custom middleware pitfalls. Use when reviewing Program.cs pipeline setup or custom middleware.From its SKILL.md
npx -y skills add Sarmkadan/dotnet-senior-skills --skill middleware-and-pipeline-orderAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- 29 days oldThe repository was created 29 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
5.1 KB, ~1.0k tokens by cl100k_base, as published. Nobody here has run it
Middleware and Pipeline Order
Order is the contract
Middleware runs in registration order on the way in, reverse on the way out. Most pipeline bugs are ordering bugs, and they fail silently - a misplaced middleware doesn't throw, it just doesn't apply. The canonical order:
app.UseExceptionHandler(); // first: catches everything below
app.UseHsts(); // prod only
app.UseHttpsRedirection();
app.UseStaticFiles(); // before auth deliberately - or after, if files need auth (decide!)
app.UseRouting();
app.UseCors(); // after routing, before auth
app.UseAuthentication(); // WHO you are
app.UseAuthorization(); // WHAT you may do - always after authentication
app.MapControllers();
Review flags with their failure modes:
UseAuthorizationbeforeUseAuthentication: authorization evaluates an anonymous principal -[Authorize]returns 401 for valid tokens, or worse, policies keyed on claims all fail open/closed confusingly.UseCorsafterUseAuthentication: preflight OPTIONS requests (which carry no credentials) hit auth and die with 401 - the browser reports it as a CORS error and someone "fixes" it by allowing anonymous.UseExceptionHandleranywhere but first: exceptions from middleware above it escape as raw 500s with no ProblemDetails and no logging.UseStaticFilesplacement is a decision, not a default: before auth means every file is public - fine for CSS, an incident for/files/contracts/. Files needing authorization are served through an endpoint, notUseStaticFiles.
Endpoint-level cross-cutting: filters, not middleware
Middleware sees HttpContext and runs for every request; it does not know which endpoint matched (before UseRouting) or its metadata. Logic that needs the endpoint, model state, or action arguments belongs in endpoint filters / action filters / [Authorize] policies. A middleware doing if (context.Request.Path.StartsWithSegments("/api/admin")) is hand-rolled routing - it silently misses /API/Admin (or doesn't, depending on case settings), breaks on route changes, and is invisible to endpoint metadata tooling. Path-prefix auth checks in middleware are a rejection; that is what authorization policies applied to route groups are for.
Custom middleware pitfalls
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
// WRONG: reading the body without buffering consumes it - model binding downstream gets an empty stream
using var reader = new StreamReader(context.Request.Body);
var body = await reader.ReadToEndAsync();
await next(context);
}
- Reading the request body:
context.Request.EnableBuffering()first, read, thenBody.Position = 0. And cap what you read - buffering a 500MB upload to log it is self-DoS. - Writing to the response after
next()when the response has started (context.Response.HasStarted) throws. Headers must be set before the first body byte; useOnStartingcallbacks for late headers. - Constructor-injected scoped services in conventional middleware: middleware is a singleton; scoped dependencies go as
InvokeAsyncparameters (the framework resolves them per request). Constructor injection of aDbContextinto middleware is the captive-dependency bug (DI skill) in its most common costume. - Swallowing exceptions in middleware "to keep the pipeline alive": the exception handler above you exists for that; catch only what you can translate, rethrow the rest.
- Not calling
next()is legitimate short-circuiting (auth, rate limiting) - but then you own writing a complete response, including status code and content type.
The pipeline is per-request; branches are static
app.UseWhen(ctx => ...) / app.Map(...) build the branch once at startup - the predicate runs per request, the builder lambda does not. Feature-flag checks inside the builder lambda evaluate once at boot and never again; per-request toggling goes inside the middleware itself.
Review checklist
- Every middleware between
UseRoutingandMapXjustified - that slot is for things needing route data (CORS, auth, rate limiting, output cache). app.UseDeveloperExceptionPage()/ Swagger UI insideif (app.Environment.IsDevelopment())- exception detail pages in production leak stack frames and connection strings (exception skill).- Response-buffering middleware (compression, caching) declared before things that write - order determines whether they see the bytes.
- Rate limiting (
UseRateLimiter) present on auth endpoints and anything expensive-per-call; after auth if limits are per-user, before if per-IP. - Health checks (
MapHealthChecks) excluded from auth, logging noise, and rate limits deliberately - and not running real dependency checks on the liveness probe (a DB blip should fail readiness, not get the pod killed).
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.