Junior 7 min · March 06, 2026

Reversed Auth Middleware Order — Silent 401 in ASP.NET Core

A valid JWT yields 401 when UseAuthorization runs before UseAuthentication.

N
Naren Founder & Principal Engineer

20+ years shipping production .NET services in enterprise systems. Notes here come from systems that actually shipped.

Follow
Production
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • ASP.NET Core middleware is a chain of delegates that process HTTP requests and responses in a two-phase pipeline.
  • Each middleware can inspect, modify, or short-circuit the request/response.
  • Order is critical: each middleware depends on the layer above having run.
  • Performance overhead is minimal (sub-millisecond per middleware) but incorrect order silently breaks security.
  • Production insight: Putting UseAuthorization before UseAuthentication returns 401 on every authenticated request.
  • Biggest mistake: Injecting a scoped DbContext into the middleware constructor instead of InvokeAsync parameters.
✦ Definition~90s read
What is Middleware in ASP.NET Core?

Middleware in ASP.NET Core is the backbone of the request pipeline — a sequence of delegates that process every HTTP request and response as they flow through your application. Each middleware component can inspect, modify, or short-circuit the pipeline by either calling the next delegate (passing the request downstream) or returning early (e.g., sending a 401 Unauthorized response).

Imagine every HTTP request is a letter arriving at a busy office.

This composable architecture lets you layer cross-cutting concerns like authentication, logging, compression, and CORS without coupling them to your application logic. The order you register middleware in Program.cs (or Startup.Configure) is the exact order requests traverse — a reversed order means authentication runs after authorization, or error handling runs before logging, leading to silent failures like a 401 that never reaches your custom middleware because an earlier component already short-circuited the pipeline.

In practice, the pipeline is a linked list of Func<HttpContext, RequestDelegate, Task> delegates, where each middleware either calls next(context) to continue or returns a response directly. Common pitfalls include placing UseAuthentication() after UseAuthorization() (which silently returns 401 before authentication can set the user principal), or registering UseExceptionHandler() after middleware that throws exceptions (so the error handler never catches them).

The Map, Use, and Run methods control branching and termination: Use chains delegates, Run terminates the pipeline (no next), and Map branches based on request path. Understanding this order is critical because ASP.NET Core's middleware is not declarative — it's procedural, and every registration order mistake manifests as a runtime bug that's hard to trace.

When you need to short-circuit the pipeline — for example, returning a 401 from a custom authentication middleware — you simply omit the await next(context) call and write the response directly. This is how ASP.NET Core's built-in authentication middleware works: if the user isn't authenticated, it sets a 401 status and returns without calling the next delegate.

The silent 401 problem arises when middleware that should run before authentication (like logging or request validation) is registered after it, so the request never reaches those components. Debugging this requires inspecting the middleware order in Program.cs and understanding that the pipeline is a stack — the first registered middleware is the outermost layer, and the last registered is the innermost (closest to your endpoint).

Tools like the app.UseMiddleware<T>() overload with explicit ordering or the Microsoft.AspNetCore.Diagnostics package's DeveloperExceptionPage middleware can help visualize the pipeline, but the fix is always reordering registrations to match the logical flow: error handling first, then authentication, authorization, routing, and finally your custom middleware.

Plain-English First

Imagine every HTTP request is a letter arriving at a busy office. Before the letter reaches the CEO (your controller), it passes through a chain of desks — the receptionist checks if it's addressed correctly, the security guard checks for a valid ID badge, the assistant stamps it with the time it arrived. Each desk can either pass the letter forward, send it straight back, or even modify it before passing it on. That chain of desks is exactly what ASP.NET Core middleware is. Each piece of middleware is one desk in that chain — it inspects the request, optionally does something to it, and decides whether to pass it to the next desk or short-circuit and send a response right back.

Every web framework needs a way to handle the messy, repetitive work that surrounds every request — logging it, authenticating the caller, compressing the response, catching exceptions. Without a clean mechanism for this, you'd be copy-pasting the same authentication check into every single controller action. That's not just tedious; it's a reliability disaster. ASP.NET Core solves this with a first-class middleware pipeline that's fast, composable, and completely in your control.

The problem middleware solves is cross-cutting concerns. Authentication doesn't belong to any one feature — it belongs to all of them. The same goes for logging, CORS headers, rate limiting, and exception handling. Middleware lets you write that logic once, plug it into the pipeline in the right place, and trust that it runs for every request that flows through your app. The pipeline model also means concerns are layered cleanly: outer middleware wraps inner middleware, so you can handle exceptions around everything, authenticate before authorisation, and authorise before routing — all without tangling those concerns together.

By the end of this article you'll be able to draw the ASP.NET Core request pipeline from memory, explain why order matters (with a concrete example of what breaks when you get it wrong), write a custom middleware class with proper dependency injection, and use the three ways to register middleware — UseMiddleware, Use, and Map — choosing the right tool for each situation.

What Middleware in ASP.NET Core Actually Does

Middleware in ASP.NET Core is software assembled into an application pipeline to handle requests and responses. Each component chooses whether to pass the request to the next component and can perform actions before and after the next component in the pipeline. This is the core mechanic: a chain of delegates where each middleware can short-circuit the pipeline by not calling the next delegate.

In practice, middleware is registered in order in the Program.cs file using extension methods like UseAuthorization(), UseAuthentication(), and UseCors(). The order matters critically because each middleware can modify the request or response context. For example, authentication middleware must run before authorization middleware — otherwise, authorization checks will see an unauthenticated user and fail silently, returning a 401 without any diagnostic.

You use middleware to handle cross-cutting concerns like logging, exception handling, authentication, authorization, response compression, and static files. In real systems, getting the order wrong is the most common source of silent 401 errors that baffle teams. The rule is: CORS, then Authentication, then Authorization, then your custom middleware — in that exact sequence.

Order Is Not Optional
Reversing Authentication and Authorization middleware doesn't cause a compile error — it silently returns 401 on every protected endpoint, even with valid tokens.
Production Insight
Teams migrating from .NET Framework often place UseAuthorization before UseAuthentication, causing all authenticated requests to fail with 401.
The symptom is that the authorization middleware runs first, sees no user principal, and immediately returns 401 without ever calling the authentication middleware.
Rule of thumb: always register middleware in the order they should process the request — authentication first, then authorization, then your business logic.
Key Takeaway
Middleware order is the pipeline — reversing two components changes behavior silently.
Authentication must always precede authorization in the pipeline.
Short-circuiting (not calling next) is how middleware enforces security — but only if it runs in the right order.
ASP.NET Core Middleware Pipeline Order THECODEFORGE.IO ASP.NET Core Middleware Pipeline Order Correct ordering of middleware to avoid silent 401 errors Request Entry HTTP request arrives at pipeline Exception Handling UseExceptionHandler / DeveloperExceptionPage Authentication app.UseAuthentication() Authorization app.UseAuthorization() Custom Middleware Business logic or short-circuit Response Output Endpoint middleware or static files ⚠ Reversed auth order causes silent 401 Always place UseAuthentication before UseAuthorization THECODEFORGE.IO
thecodeforge.io
ASP.NET Core Middleware Pipeline Order
Middleware Aspnet Core

How the ASP.NET Core Request Pipeline Actually Works

The pipeline is a linked chain of delegates. When a request arrives, ASP.NET Core calls the first delegate. That delegate can do work, then call 'next()' to invoke the next delegate in the chain, then do more work after next() returns. This means every piece of middleware has two opportunities to act: once on the way in (before calling next) and once on the way out (after next returns). That's the key mental model — it's not a one-way conveyor belt, it's a stack of nested function calls.

The chain terminates at a 'terminal middleware' — something like UseEndpoints — that handles the request and writes a response without calling next. If no terminal middleware matches, ASP.NET Core returns a 404.

Each middleware is registered in Program.cs using extension methods on WebApplication. The order you call those methods is the order they execute. This isn't a detail — it's the most important rule in ASP.NET Core middleware. Authentication must come before Authorisation. Exception handling must wrap everything else. We'll see exactly what breaks when that order is wrong in the Gotchas section.

PipelineVisualization.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Program.cs — stripped-down app to visualise the two-phase pipeline
// Run this and watch the console output to see requests flow IN and OUT

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Middleware A — outermost layer (first in, last out)
app.Use(async (context, next) =>
{
    Console.WriteLine("[A] Incoming — path: " + context.Request.Path);
    
    await next(context);   // hand control to the next middleware
    
    // This line runs AFTER the inner middlewares have all finished
    Console.WriteLine("[A] Outgoing — status: " + context.Response.StatusCode);
});

// Middleware B — middle layer
app.Use(async (context, next) =>
{
    Console.WriteLine("[B] Incoming");
    await next(context);
    Console.WriteLine("[B] Outgoing");
});

// Terminal middleware — writes the response, does NOT call next
app.Run(async context =>
{
    Console.WriteLine("[Terminal] Writing response");
    context.Response.StatusCode = 200;
    await context.Response.WriteAsync("Hello from the pipeline!");
});

app.Run();
Output
[A] Incoming — path: /
[B] Incoming
[Terminal] Writing response
[B] Outgoing
[A] Outgoing — status: 200
The Stack Mental Model
Read your middleware registrations like a stack: the first one registered is the outermost wrapper. Whatever you put first in Program.cs gets to inspect both the raw incoming request AND the final outgoing response. That's why UseExceptionHandler goes first — it needs to catch exceptions thrown by everything else beneath it.
Production Insight
The two-phase pipeline is not just a mental model—it's how UseExceptionHandler catches exceptions.
If an exception is thrown in the request phase, the exception handler on the outer layer still catches it during the outgoing phase.
Rule: Always place exception handling middleware as the first registration to ensure it wraps all downstream errors.
Key Takeaway
The pipeline is a stack: first registered = outermost wrapper.
Code after await next() runs after all inner middleware have completed.
This two-phase design enables patterns like timing, logging, and exception handling.

Writing a Real Custom Middleware Class (With Dependency Injection)

Inline lambdas with app.Use() are fine for trivial cases, but anything non-trivial should be a dedicated class. A middleware class gives you a testable unit, proper constructor injection, and a home for the logic that doesn't clutter Program.cs.

The convention ASP.NET Core uses for middleware classes is simple: your class needs a constructor that accepts a RequestDelegate (the next middleware), and an InvokeAsync method that accepts HttpContext. That's the entire contract. You don't implement any interface — the framework discovers the method by convention.

Scoped services can't be injected via the constructor because middleware is instantiated once (singleton lifetime). Instead, inject scoped services as parameters of InvokeAsync — the framework handles that automatically. This is a common interview question and a common production bug. The example below builds a request-timing middleware that logs how long each request takes, which is something almost every production app needs.

RequestTimingMiddleware.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// RequestTimingMiddleware.cs
// Measures how long each request takes and logs it.
// ILogger<T> is a singleton-safe service — safe to inject in constructor.
// A scoped service like a DbContext would go on InvokeAsync instead.

using System.Diagnostics;

public class RequestTimingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestTimingMiddleware> _logger;

    // Constructor injection: only use singleton or transient services here
    public RequestTimingMiddleware(
        RequestDelegate next,
        ILogger<RequestTimingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    // InvokeAsync is called by the framework for every request
    // Scoped services (e.g. AppDbContext) can be injected here as parameters
    public async Task InvokeAsync(HttpContext context)
    {
        var stopwatch = Stopwatch.StartNew();

        // --- INCOMING: before the rest of the pipeline runs ---
        _logger.LogInformation(
            "Request started: {Method} {Path}",
            context.Request.Method,
            context.Request.Path);

        await _next(context); // run the rest of the pipeline

        // --- OUTGOING: after the response has been produced ---
        stopwatch.Stop();

        _logger.LogInformation(
            "Request finished: {Method} {Path} — {StatusCode} in {ElapsedMs}ms",
            context.Request.Method,
            context.Request.Path,
            context.Response.StatusCode,
            stopwatch.ElapsedMilliseconds);
    }
}

// MiddlewareExtensions.cs
// A clean extension method makes registration readable in Program.cs
public static class RequestTimingMiddlewareExtensions
{
    public static IApplicationBuilder UseRequestTiming(
        this IApplicationBuilder app)
    {
        return app.UseMiddleware<RequestTimingMiddleware>();
    }
}

// Program.cs — how to wire it all up
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();

var app = builder.Build();

// Register exception handling FIRST so it wraps everything
app.UseExceptionHandler("/error");

// Our custom timing middleware — early so it times the full request
app.UseRequestTiming();

app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

app.Run();
Output
info: RequestTimingMiddleware[0]
Request started: GET /weatherforecast
info: RequestTimingMiddleware[0]
Request finished: GET /weatherforecast — 200 in 42ms
Watch Out: Scoped Services in Constructors
If you inject a scoped service (like Entity Framework's DbContext) into your middleware constructor, you'll get an InvalidOperationException at startup or, worse, a silent scope leak in production. Always inject scoped services as parameters on InvokeAsync, not the constructor. The framework resolves them from the correct per-request scope automatically.
Production Insight
Injecting scoped services via constructor causes an InvalidOperationException at startup.
Even if it doesn't crash, the shared instance across requests leads to corrupted per-request data.
Rule: Always place scoped services on InvokeAsync—the framework resolves them from the request scope.
Key Takeaway
Middleware class convention: RequestDelegate in constructor, InvokeAsync(HttpContext).
Scoped services go on InvokeAsync, not constructor.
Extension methods make registration readable and follow framework patterns.

Map, Use, and Run — Choosing the Right Registration Method

ASP.NET Core gives you three core extension methods for building the pipeline, and each has a specific job. Mixing them up is a common source of subtle bugs.

'app.Use()' is the standard building block. It receives the HttpContext and a delegate to the next middleware. You call next to pass control forward. Use it for any middleware that should participate in both the incoming and outgoing phases.

'app.Run()' registers a terminal middleware — it never calls next because it's the end of the line. If you use Run and then register more middleware after it, those later registrations are silently ignored. This surprises a lot of developers.

'app.Map()' branches the pipeline based on the request path. Requests matching the path go down the branch; everything else continues on the main pipeline. It's perfect for health check endpoints, admin sections, or versioned API branches that need completely different middleware stacks. There's also 'MapWhen()' for branching on arbitrary conditions, like a header value or query string.

MiddlewareRegistrationMethods.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// Program.cs — demonstrating Use, Run, and Map in a single app

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// MAP: branch the pipeline for /health — nothing else runs for this path
app.Map("/health", healthApp =>
{
    // This Run only applies inside the /health branch
    healthApp.Run(async context =>
    {
        // Lightweight health check — no auth, no logging overhead
        context.Response.ContentType = "application/json";
        await context.Response.WriteAsync("{\"status\": \"healthy\"}");
    });
});

// USE: add a header to every response NOT handled by the /health branch
app.Use(async (context, next) =>
{
    // Runs on the way out — safe to set headers here before flush
    await next(context);
    context.Response.Headers.Append("X-Powered-By", "TheCodeForge");
});

// MAPWHEN: branch based on a condition — here, requests with a specific header
app.MapWhen(
    context => context.Request.Headers.ContainsKey("X-Internal-Request"),
    internalApp =>
    {
        internalApp.Run(async context =>
        {
            await context.Response.WriteAsync("Internal route — restricted access");
        });
    });

// RUN: terminal middleware for all remaining requests
// IMPORTANT: anything registered after this is NEVER reached
app.Run(async context =>
{
    await context.Response.WriteAsync("Main application response");
});

// This middleware is DEAD CODE — app.Run above is terminal
// The framework silently ignores it. Don't do this.
app.Use(async (context, next) =>
{
    Console.WriteLine("This never executes!");
    await next(context);
});

app.Run(); // starts the web server (different Run — on WebApplication, not IApplicationBuilder)
Output
GET /health → {"status": "healthy"}
GET /anything-else → "Main application response" + header X-Powered-By: TheCodeForge
GET / (X-Internal) → "Internal route — restricted access"
Pro Tip: Map for Micro-Pipelines
Use app.Map() when a section of your app needs genuinely different middleware. A /metrics endpoint, for example, shouldn't go through authentication middleware — it should have its own IP-allow-list check. Map lets you build a completely separate pipeline branch rather than cluttering your main pipeline with conditional logic.
Production Insight
app.Run() is terminal—any middleware registered after it is silently ignored.
A common mistake is placing middleware after app.Run() and wondering why it doesn't execute.
Rule: Reserve app.Run() for the final handler; use app.Use() for middleware that should continue.
Key Takeaway
Use for two-phase middleware, Run for terminal, Map for branches.
Map creates an isolated sub-pipeline; main pipeline middleware does not run in the branch.
MapWhen branches on conditions—great for admin routes or header-based logic.

Middleware Order in Practice — The Correct ASP.NET Core Pipeline

The single most confusing thing about ASP.NET Core middleware for developers coming from other frameworks is that order is everything, and it's silent about it. There's no error if you put UseAuthorization before UseAuthentication — it just doesn't work. You get 401s or 403s that seem random until you understand the pipeline.

Microsoft documents a recommended order and it exists for good reason. Exception handling must wrap everything to catch exceptions from routing, authentication, and your own code. HTTPS redirection should happen before static files so you don't serve static assets over HTTP. Authentication must happen before Authorisation because Authorisation reads the identity that Authentication establishes. Routing must happen before Authorisation so the framework knows which endpoint's policies to check.

The table below compares the correct order against a common incorrect ordering and shows exactly what symptom you'd see. Once you understand the 'why' behind each position, the order becomes easy to remember — it's not arbitrary, it's causal.

CorrectMiddlewareOrder.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// Program.cs — the recommended middleware order for a production API
// Each comment explains WHY it's in this position, not just WHAT it does

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();
builder.Services.AddControllers();

var app = builder.Build();

// 1. EXCEPTION HANDLER — outermost layer, catches exceptions from everything below
//    In development, use the developer exception page for stack traces instead
if (app.Environment.IsDevelopment())
    app.UseDeveloperExceptionPage();
else
    app.UseExceptionHandler("/error");

// 2. HSTS + HTTPS REDIRECTION — security headers before any content is served
app.UseHsts();
app.UseHttpsRedirection();

// 3. STATIC FILES — served before routing to avoid routing overhead for assets
//    Static files are terminal for matched paths — they never hit your controllers
app.UseStaticFiles();

// 4. ROUTING — parses the URL and selects the endpoint
//    Must come before UseAuthentication so endpoint metadata is available
//    (used by some auth handlers to check endpoint-level attributes)
app.UseRouting();

// 5. CORS — must come after UseRouting, before UseAuthentication
//    Preflight OPTIONS requests need CORS headers before auth checks
app.UseCors("AllowFrontend");

// 6. AUTHENTICATION — reads the token/cookie and builds the ClaimsPrincipal
//    MUST come before UseAuthorization
app.UseAuthentication();

// 7. AUTHORISATION — checks if the established identity has permission
//    Relies on UseAuthentication having already run
app.UseAuthorization();

// 8. ENDPOINT EXECUTION — actually runs your controller actions / minimal API handlers
app.MapControllers();

app.Run();
Output
Application starts successfully.
GET /api/secure (valid JWT) → 200 OK
GET /api/secure (no JWT) → 401 Unauthorized
GET /index.html → 200 OK (served from wwwroot, never hits routing)
GET /throws-exception → 500 with ProblemDetails JSON (caught by UseExceptionHandler)
Interview Gold
If an interviewer asks 'what happens if you put UseAuthorization before UseAuthentication?' — the answer is: authorization runs before a ClaimsPrincipal has been established, so context.User is always an unauthenticated anonymous identity. Every [Authorize] attribute silently fails and returns 401, even with a valid token in the request. It's one of the hardest bugs to diagnose if you don't know the pipeline.
Production Insight
Placing UseAuthorization before UseAuthentication compiles fine but always returns 401.
This silent failure is the hardest middleware bug to debug because no exception is thrown.
Rule: Always follow the recommended order—exception, HSTS, HTTPS, static, routing, CORS, auth, endpoints.
Key Takeaway
Order is causal: each layer depends on the previous.
Authentication builds ClaimsPrincipal; Authorization reads it.
Exception handler must be outermost to catch everything.

Short-Circuiting the Pipeline: When to Return Early Without Calling next()

Middleware isn't required to call next(). If it sets a response and returns without calling next, the pipeline short-circuits — inner middleware never runs, and outer middleware's post-next code also doesn't execute. This is exactly what authentication middleware does when a token is invalid: it returns 401 immediately without passing the request to your controllers.

Short-circuiting is powerful for early rejection patterns: maintenance mode checks, IP whitelisting, request size limits, and authentication failures all legitimately stop the pipeline early. But there's a trade-off: if you have timing or logging middleware registered before the short-circuiting middleware, their outgoing-phase code won't run. That means no log entry for rejected authentication attempts unless the short-circuiting middleware handles logging itself.

The decision to short-circuit should be deliberate. Use it when you know the request cannot possibly be fulfilled. But if you only want to modify the response (e.g., add a header) without affecting inner pipeline logic, always call next() first and modify the response after.

ShortCircuitExample.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Program.cs — demonstrating short-circuiting middleware

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Timing middleware — logs start/end
app.Use(async (context, next) =>
{
    Console.WriteLine("Timing: Incoming");
    var sw = Stopwatch.StartNew();
    await next(context);
    sw.Stop();
    Console.WriteLine($"Timing: {sw.ElapsedMilliseconds}ms");
});

// Short-circuit middleware: block requests from certain IPs
app.Use(async (context, next) =>
{
    var blockedIPs = new[] { "192.168.1.100" };
    if (blockedIPs.Contains(context.Connection.RemoteIpAddress?.ToString()))
    {
        context.Response.StatusCode = 403;
        await context.Response.WriteAsync("Blocked");
        return; // short-circuit — no next() call
    }
    await next(context); // pass through for allowed IPs
});

app.Run(async context =>
{
    await context.Response.WriteAsync("Hello from the app!");
});

app.Run();
Output
Request from allowed IP:
Timing: Incoming
Timing: 12ms (includes response generation)
Request from blocked IP 192.168.1.100:
Timing: Incoming
(No Timing output — post-next code never ran because next was never called)
Response: 403 Blocked
Short-Circuiting Breaks Outer Logging
If your timing / logging middleware is registered before a short-circuiting middleware, the 'outgoing' code (after await next()) never executes for requests that are short-circuited. Your monitors will miss those requests. To capture them, either: (1) register logging middleware after the short-circuit middleware, or (2) have the short-circuit middleware manually emit its own log entry before returning.
Production Insight
Short-circuiting middleware (like authentication failure) prevents outer middleware's post-next code from running.
This means timing/ logging middleware will not record the rejected request's duration.
Rule: If you rely on outer middleware for observability, ensure short-circuiting middleware logs its own telemetry or move logging inside the short-circuit path.
Key Takeaway
Call next() to continue pipeline, or skip to short-circuit.
Short-circuiting prevents inner and outer post-pipeline code from running.
Use short-circuiting for early rejection; accept that outer middleware may miss the response.

Middleware Dependencies — Singleton, Scoped, Transient: Choose or Lose

Middleware constructors are singleton by default. They live for the lifetime of the application. That means any scoped service injected via the constructor is effectively a singleton — you'll get the same instance for every request.

This is a production trap. You register a scoped IInvoiceContext, inject it into middleware, and wonder why your second request has stale data. Because it does. The DI container resolves scoped services at startup when the middleware is instantiated, not per request.

InvokeAsync takes a scoped parameter directly from DI for each request. Use that pattern. Never inject scoped services into the constructor. Transient services that carry state? Same rule. Constructor is for singletons only. If you need a fresh instance of something per request, pull it in the method signature.

Here's the litmus test: if your middleware touches a database, reads user-specific data, or writes to a scoped cache, it belongs in InvokeAsync, not the constructor. If it's static configuration or a logger? Fine in the constructor. Everything else is a bug waiting to happen.

ScopedMiddlewareFix.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// io.thecodeforge — csharp tutorial

public class InvoiceAuthMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<InvoiceAuthMiddleware> _logger; // singleton: OK

    public InvoiceAuthMiddleware(RequestDelegate next, ILogger<InvoiceAuthMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context, IInvoiceContext invoiceContext)
    {
        // invoiceContext is scoped — resolved fresh per request from DI
        var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault();
        if (tenantId != null)
        {
            invoiceContext.SetTenant(tenantId);
        }

        await _next(context);
    }
}
Output
No output — middleware runs silently. If you inject IInvoiceContext in the constructor, you'll get a runtime exception from DI or stale data.
Production Trap: The Constructor Injection Lie
If you inject scoped services into any middleware constructor, the app will either crash at startup with a DI resolution error (ASP.NET Core 6+) or silently reuse the first request's instance. Always use InvokeAsync for scoped or transient dependencies.
Key Takeaway
Inject singletons in constructor, scoped and transient services in InvokeAsync method signature.

Branch the Pipeline with Map — Routes, Not Religion

Not every path through your app needs the same middleware. Logging headers? Yes, everywhere. Checking authentication for the admin panel? Only under /admin. Rendering static files? Only for paths that map to actual files.

Branching middleware pipelines is how you avoid paying for work you don't need. Map splits the pipeline based on a path match. When the request hits /healthz, you don't need rate limiting, auth, or request logging — just return 200 and get out.

MapWhen gives you more control — predicate on anything: query string, header value, random coin flip. But don't get cute. Branching should reflect explicit app boundaries: admin area, API version, health probes. Use Map for static path branches, MapWhen only when you absolutely need conditional logic.

Every branch is a fresh pipeline. That means if you branch after CORS and before auth, the branch doesn't get auth. Plan your branch points carefully. The middleware order rules don't change per branch — each branch is independent. Common pitfall: branching early and forgetting to re-add error handling. Your health check gets no exception handler. One unhandled exception later, you remember.

If a branch doesn't call _next, it's terminal. That's the whole point.

BranchingPipeline.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// io.thecodeforge — csharp tutorial

var app = builder.Build();

app.UseExceptionHandler("/error"); // global — runs before any branch

app.Map("/healthz", healthApp =>
{
    healthApp.Run(async context =>
    {
        context.Response.StatusCode = 200;
        await context.Response.WriteAsync("Healthy");
    });
});

app.MapWhen(
    ctx => ctx.Request.Headers["X-Debug"].Any(),
    debugApp =>
    {
        debugApp.Use(async (context, next) =>
        {
            // debug middleware: only on requests with X-Debug header
            var stopwatch = Stopwatch.StartNew();
            await next();
            stopwatch.Stop();
            context.Response.Headers["X-Elapsed-Ms"] = stopwatch.ElapsedMilliseconds.ToString();
        });
    });

app.Run();
Output
GET /healthz -> 200 "Healthy"
GET /api/orders (with X-Debug: 1) -> response, X-Elapsed-Ms header added
GET /api/orders (without header) -> runs normally, no extra header
Senior Shortcut: Map vs MapWhen
Use Map for path-based branches — it's cleaner and the pipeline abstraction is obvious. MapWhen is for runtime conditions. If you use MapWhen for something that could be Map, you're making the team guess what the predicate does.
Key Takeaway
Branch middleware by app boundary (admin, health, API version) using Map. Use MapWhen only for non-path conditions like headers or query strings.
● Production incidentPOST-MORTEMseverity: high

The Silent 401: When Middleware Order Strikes at 3 AM

Symptom
After deploying a new version, every API call with a valid JWT returned 401 Unauthorized. The authentication logs showed the token was valid, and the debugger confirmed the identity was correctly set in UseAuthentication.
Assumption
The team assumed the JWT configuration was correct (issuer, secret, audience) and that the [Authorize] attribute was being enforced properly. They spent hours checking token validation parameters.
Root cause
The middleware order in Program.cs had been inadvertently changed during a refactor: UseAuthorization() was placed before UseAuthentication(). The authorization middleware checked context.User, which was still an anonymous identity because authentication hadn't run yet. No exception, no warning — every request that hit an [Authorize] endpoint returned 401.
Fix
Reordered the middleware to place UseAuthentication() immediately before UseAuthorization(). The recommended pipeline: exception handler → routing → cors → authentication → authorization → endpoints. Deployed the fix and all endpoints returned 200.
Key lesson
  • The order of middleware is causal, not decorative: authentication builds the user identity; authorization reads it. Reversing them is a silent failure.
  • Always run a simple integration test after any middleware registration change — send a request with a valid token and assert the response is not 401.
  • Use code analysis or a custom middleware that logs the pipeline order at startup to catch accidental reordering.
Production debug guideSymptom → Action: Diagnose common middleware problems fast5 entries
Symptom · 01
Middleware registered in Program.cs never executes
Fix
Check if an app.Run() call appears before it. app.Run() is terminal — everything after it is silently ignored. Ensure non-terminal middleware uses app.Use() not app.Run().
Symptom · 02
All requests return 401 even with valid token
Fix
Verify middleware order: UseAuthentication() must be before UseAuthorization(). Also confirm that CORS middleware is after UseRouting and before UseAuthentication.
Symptom · 03
InvalidOperationException at startup: 'Cannot consume scoped service from singleton'
Fix
You injected a scoped service (e.g., DbContext) into the middleware constructor. Move it to the InvokeAsync method parameter. The framework resolves scoped services per request via DI.
Symptom · 04
CORS preflight requests (OPTIONS) fail with 405
Fix
Ensure UseCors() is placed after UseRouting() and before UseAuthentication(). Preflight requests should not require authentication. Also verify the CORS policy allows the requested origin and method.
Symptom · 05
Response headers set in middleware not appearing in client
Fix
Check if a middleware after yours is overriding or clearing headers. Also ensure headers are set before the response starts streaming (before calling WriteAsync). Use OnStarting callback to set headers just before flush.
★ Middleware Quick Debug Cheat SheetCommon middleware failures and the exact commands & fixes to resolve them in minutes
All authenticated requests return 401
Immediate action
Check middleware order: UseAuthentication must precede UseAuthorization.
Commands
curl -v -H 'Authorization: Bearer <token>' http://localhost:5000/api/secure | grep HTTP
Check Program.cs for middleware registration sequence. Use dotnet list configuration to verify appsettings.
Fix now
Reorder middleware to follow Microsoft's recommended pipeline: exception → HSTS → HTTPS → static → routing → CORS → auth → endpoints.
Middleware throws 'Cannot consume scoped service from singleton'+
Immediate action
Move scoped service injection from constructor to InvokeAsync parameters.
Commands
In your middleware class, change 'public MyMiddleware(RequestDelegate next, MyDbContext db)' to 'public async Task InvokeAsync(HttpContext context, MyDbContext db)'
Remove the scoped parameter from constructor and keep only singleton-safe services (ILogger, IOptions).
Fix now
Replace constructor injection with InvokeAsync parameter injection and rebuild.
Middleware registered after app.Run() never runs+
Immediate action
Move all middleware before the terminal app.Run() call.
Commands
grep -n 'app\.Run\|app\.Use\|app\.Map' Program.cs to see registration order
Inspect the last registration: if it's app.Run(), everything after is dead code.
Fix now
Restructure: use app.Use() for all non-terminal middleware, and place app.Run() only at the end with a fallback handler.
Aspectapp.Use()app.Run()app.Map()
Calls next middlewareYes — you call next(context) explicitlyNo — terminal, never calls nextNo — branches to a sub-pipeline
Typical use caseLogging, auth, headers, timingFallback 404 handler, simple endpointsHealth checks, admin section, API versions
Sees outgoing responseYes — code after await next() runs on the way outNo — it IS the responseOnly within the branch
Silent gotchaForgetting to call next() accidentally terminates the pipelineMiddleware registered after it is silently ignoredThe branch is completely isolated — main pipeline middleware doesn't run in it
Scoped DI servicesInject via InvokeAsync parameter (class-based)N/A for inline lambdas; class-based same rule appliesSame rules apply within the branch
TestabilityHigh — extract to class, inject ILogger etc.Low for inline lambdasHigh — branch pipeline is independently testable

Key takeaways

1
The pipeline is a two-phase stack, not a one-way conveyor
every middleware that calls next() gets to act on both the incoming request (before next) and the outgoing response (after next). This is how UseExceptionHandler and timing middleware work.
2
Order is everything and it's silent
UseAuthorization before UseAuthentication compiles and starts fine but returns 401 for every authenticated request. The recommended order exists because each layer depends on the layer above it having already run.
3
Scoped services belong on InvokeAsync parameters, not constructors
middleware instances live for the lifetime of the app (singleton-ish), so injecting a scoped DbContext into the constructor causes a scope capture bug. The InvokeAsync parameter injection is the correct, framework-supported pattern.
4
app.Run() silently swallows everything registered after it
it's terminal. If you're debugging middleware that seems to never run, check whether an app.Run() call appears before it in Program.cs.
5
Short-circuiting middleware (e.g., authentication failure) prevents outer middleware's post-next code from running
account for this in logging/timing middleware registering before the short-circuit point.
6
Extension methods (UseMiddleware<T>) encapsulate registration logic and improve testability
always use them for custom middleware classes.

Common mistakes to avoid

3 patterns
×

Putting UseAuthorization before UseAuthentication

Symptom
Every request returns 401 even with a valid JWT or cookie, regardless of the [Authorize] policy. No error in logs.
Fix
Always place UseAuthentication() immediately before UseAuthorization(). Authentication sets up context.User; Authorisation reads it.
×

Injecting a scoped service (e.g., DbContext) into the middleware constructor

Symptom
InvalidOperationException at startup 'Cannot consume scoped service from singleton', or worse, no exception but a shared DbContext across requests causing data corruption in production.
Fix
Add the scoped service as a parameter on InvokeAsync(HttpContext context, MyDbContext db) — the framework resolves it from the correct per-request scope.
×

Registering middleware after app.Run()

Symptom
Middleware registered after app.Run() is completely silently ignored. No warning, no exception.
Fix
Remember that app.Run() is terminal — it never calls next. Any middleware that should run for all requests must be registered before app.Run(). Use app.Use() for non-terminal middleware and reserve app.Run() for the very last handler only.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Can you explain the ASP.NET Core request pipeline and why middleware ord...
Q02SENIOR
How does dependency injection work differently in a middleware construct...
Q03JUNIOR
What's the difference between app.Use(), app.Run(), and app.Map()? If I ...
Q01 of 03SENIOR

Can you explain the ASP.NET Core request pipeline and why middleware order matters? Give a concrete example of what breaks when the order is wrong.

ANSWER
The pipeline is a chain of middleware delegates. Each middleware can perform work before and after calling the next delegate. Order is critical because each middleware depends on the state established by previous ones. For example, if you place UseAuthorization before UseAuthentication, the authorization middleware will find an unauthenticated identity and return 401 for every request, even with a valid token. This compiles without error and runs without exceptions — making it one of the hardest bugs to diagnose. The correct order is: exception handling, HTTPS redirection, static files, routing, CORS, authentication, authorization, and finally endpoint execution.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
What is middleware in ASP.NET Core and how is it different from filters?
02
Can I run async code inside middleware?
03
How do I conditionally skip a middleware for certain routes?
04
How do I unit test custom middleware?
N
Naren Founder & Principal Engineer

20+ years shipping production .NET services in enterprise systems. Notes here come from systems that actually shipped.

Follow
Verified
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
🔥

That's ASP.NET. Mark it forged?

7 min read · try the examples if you haven't

Previous
REST API with ASP.NET Core
3 / 14 · ASP.NET
Next
Entity Framework Core Basics