Senior 10 min · March 06, 2026

ASP.NET Core Middleware Ordering - Auth Bypass Gotcha

Production logs showed successful 500 responses with no auth claims, leaking SQL strings.

N
Naren Founder & Principal Engineer

20+ years shipping production .NET services in enterprise systems. Everything here is grounded in real deployments.

Follow
Production
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Middleware pipeline is a chain of request delegates processed in order
  • Each middleware can handle, short-circuit, or pass the request to the next component
  • The pipeline is built at app startup via closures and immutable linked-list of delegates
  • Order is critical — wrong order causes silent authentication bypasses or broken CORS
  • Performance: pipeline overhead is <1µs per middleware with no async overhead if synchronous
  • Production trap: exception middleware placed after logging won't catch unhandled errors
✦ Definition~90s read
What is Middleware Pipeline in .NET?

ASP.NET Core middleware is the fundamental building block of request processing in the framework. Each piece of middleware is a component that can inspect, modify, or short-circuit an HTTP request and response as they flow through a pipeline. The pipeline is constructed in Startup.Configure (or via WebApplication in .NET 6+) by chaining middleware calls with Use, Run, and Map extensions.

Imagine a airport security line where every passenger (HTTP request) must pass through a series of checkpoints — passport control, baggage scan, body scanner — in a fixed order.

The order you register them is the order they execute—both on the way in (request) and on the way out (response). This is not a suggestion; it's a hard architectural constraint. Get the order wrong, and you can silently bypass authentication, authorization, CORS, or even crash your app with a null reference exception when a middleware expects data from an earlier component that never ran.

The critical gotcha that burns teams in production is the auth bypass. If you place app.UseAuthorization() before app.UseAuthentication(), the authorization middleware will run without an authenticated user context—effectively granting access to unauthenticated requests because the identity is never established.

Similarly, putting app.UseCors() after app.UseAuthorization() means CORS headers won't be set for unauthorized requests, causing browsers to reject valid cross-origin calls with opaque errors. The one rule that prevents 95% of these bugs: register error handling first, then static files, then authentication, then authorization, then CORS, then custom middleware, then MVC/endpoints.

This ordering is not arbitrary—it mirrors the logical dependency chain where each middleware relies on state set by its predecessors.

Short-circuiting is another common failure point. Middleware can terminate the pipeline early by not calling next()—this is how static file middleware serves files without hitting your controllers. But if you short-circuit before authentication runs, you've opened a hole.

In production, this often manifests as 'it works on my machine' because local dev environments might not enforce the same pipeline order. The performance trap is subtler: ill-ordered middleware can cause redundant processing (e.g., running authentication on every static file request) or force the pipeline to execute expensive middleware even when a downstream component will short-circuit.

Real-world examples include placing app.UseHttpsRedirection() after static files, causing redirects on every asset request, or putting app.UseExceptionHandler() after MVC, so unhandled exceptions in controllers never hit your error handler. The pipeline is a directed graph of responsibility—order it like a dependency tree, not a wish list.

Plain-English First

Imagine a airport security line where every passenger (HTTP request) must pass through a series of checkpoints — passport control, baggage scan, body scanner — in a fixed order. Each checkpoint can wave you through, send you back, or redirect you to a different gate entirely. That's the middleware pipeline. Each 'checkpoint' is a piece of middleware, and the order you set them up is the order every single request walks through. Miss a checkpoint or put them in the wrong order and things go wrong fast.

Every ASP.NET Core application — whether it's a tiny API or a massive e-commerce platform — has a middleware pipeline at its core. This isn't a feature you opt into. It IS the framework. When a request arrives, it doesn't teleport to your controller. It walks a gauntlet of middleware components you configured, each one getting a shot to inspect, transform, short-circuit, or enrich it before the next component sees it. Performance profiling, authentication failures, CORS errors, missing headers — most of these trace back to middleware behaviour that wasn't fully understood.

The problem middleware solves is cross-cutting concerns — things that every request needs but that have nothing to do with your business logic. Logging, authentication, exception handling, response compression: you don't want this code scattered across every controller action. Middleware lets you define these concerns once, stack them in a precise order, and have the runtime weave them automatically around every request and response that flows through your app.

By the end of this article you'll understand exactly how the pipeline is constructed at startup, how request delegates chain together under the hood using closures, why middleware ordering is non-negotiable, how to write production-quality custom middleware, and what performance traps to avoid. You'll also be ready to answer the middleware questions that trip people up in senior .NET interviews.

How the ASP.NET Core Middleware Pipeline Actually Works

The ASP.NET Core middleware pipeline is a sequential chain of delegates that process every HTTP request and response. Each middleware component can inspect, modify, short-circuit, or pass the request to the next component via a next delegate. The pipeline is built at application startup using Use, Run, and Map extension methods, and the order of registration is the order of execution — both inbound and outbound. This is not a suggestion; it is a hard rule enforced by the framework.

Key properties: middleware executes in a nested fashion — inbound processing goes forward through the pipeline, then the response unwinds backward. This means authentication middleware placed after a static file middleware will never run for static file requests. The pipeline is essentially a linked list of Func<HttpContext, Func<Task>, Task> delegates, giving O(n) overhead where n is the number of middleware components. Each component can short-circuit by not calling next, which immediately terminates the pipeline and sends a response.

Use this pattern when you need cross-cutting concerns like logging, authentication, caching, or exception handling that must apply to every request. The ordering is critical: place error handling first, then static files, authentication, authorization, custom business logic, and finally the endpoint middleware. Misordering is the single most common source of security bypasses and silent failures in production ASP.NET Core applications.

Order Is Not Optional
Placing authentication after a middleware that short-circuits (like static files) means unauthenticated users can access those resources — the auth middleware never runs.
Production Insight
A team placed CORS middleware after authentication, causing preflight OPTIONS requests to fail with 401 instead of 204, breaking all cross-origin calls.
The symptom: browser console shows CORS errors even though the server logs show the correct CORS headers being set — but only after the auth challenge.
Rule of thumb: CORS, error handling, and static files must always be the outermost layers; authentication and authorization must come before any business logic that relies on identity.
Key Takeaway
Middleware order is the pipeline's execution order — both inbound and outbound — and is set at startup.
Short-circuiting (not calling next) is how you implement early-exit policies like authentication failures or static file serving.
Always place error handling first, then CORS, static files, authentication, authorization, and finally endpoint middleware.
ASP.NET Core Middleware Ordering THECODEFORGE.IO ASP.NET Core Middleware Ordering Pipeline flow showing correct order to avoid auth bypass Request Entry Incoming HTTP request hits pipeline Exception Handling UseExceptionHandler / DeveloperExceptionPage Authentication app.UseAuthentication() Authorization app.UseAuthorization() Endpoint Middleware app.UseEndpoints() or MVC ⚠ Auth before exception handler? Auth bypass risk! Always place exception handling before auth middleware. THECODEFORGE.IO
thecodeforge.io
ASP.NET Core Middleware Ordering
Middleware Pipeline Dotnet

1. The Middleware Pipeline: Architecture and Startup Construction

The middleware pipeline is built at application startup inside the Configure method of Startup.cs (or via WebApplication builder in .NET 6+). Each call to app.Use(), app.Run(), or app.Map() registers a delegate that will later be composed into a chain. The core data structure is a linked list of Func<RequestDelegate, RequestDelegate>. Each middleware wraps the next one in a closure.

The IApplicationBuilder interface exposes Use(Func<RequestDelegate, RequestDelegate> middleware). When you call app.UseMiddleware<T>(), it internally creates a factory that instantiates the middleware and invokes its InvokeAsync method. The order of registration directly determines the order of execution. Once app.Build() is called, the pipeline is frozen as a single RequestDelegate that internally walks the chain.

At runtime, each middleware receives the HttpContext and a RequestDelegate representing the rest of the pipeline. The middleware can inspect the request, modify it, short-circuit by not calling next, or await next to process the response on the way back. This two-way flow is the key to middleware's power — you can act both before and after downstream components.

io.thecodeforge.middleware.PipelineConstruction.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
// TheCodeForge - Middleware pipeline construction example
// Demonstrates how Use() chains delegates and how Build() creates final RequestDelegate

var app = WebApplication.Create(args);

app.Use(async (context, next) =>
{
    // Pre-processing (before downstream)
    Console.WriteLine($"Before: {context.Request.Path}");
    
    await next(context); // Call the next middleware
    
    // Post-processing (after downstream)
    Console.WriteLine($"After: {context.Response.StatusCode}");
});

app.UseMiddleware<io.thecodeforge.middleware.RequestLoggingMiddleware>();

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

app.Run();

// Internally, Build() creates a linked list of delegates
// The final pipeline is a single RequestDelegate that executes all registered middleware in order.
Production Insight
Pipeline construction is once-per-app, but middleware instances can be singleton or scoped.
Singleton middleware that depends on scoped services (like DbContext) causes captive dependency — that DbContext is captured at build time and never updated.
Fix: register middleware as transient if it injects scoped services, or use IMiddlewareFactory to create instances per request.
Key Takeaway
Middleware are chained at startup using closures.
Order of registration = order of execution.
Singleton middleware with scoped dependencies causes stale state — always transient for scoped services.

2. Short-Circuiting: When and Why It Fails in Production

Short-circuiting happens when a middleware does NOT call the next delegate. Instead, it produces a response directly and terminates the pipeline. Common examples: authentication middleware sends a 401 response, static file middleware serves a file and stops, or a custom rate-limiting middleware returns 429.

Short-circuiting is intentional and powerful, but it's easy to get wrong. The most common failure is conditional short-circuiting that accidentally fires on the wrong requests. For example, a middleware that checks for an API key but only on certain paths might skip the check entirely if the path condition is poorly written.

Another subtle bug: middleware that short-circuits but forgets to set the Content-Length header, leaving the client waiting for more data. Or middleware that writes to the response stream but forgets to call HttpResponse.Body.FlushAsync(), causing buffered data never to reach the client.

In production, short-circuiting is the root cause of many silent failures — a middleware that incorrectly short-circuits (or fails to short-circuit) can lead to security bypasses or request floods.

io.thecodeforge.middleware.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
// TheCodeForge - Short-circuit middleware with correct headers

public class MaintenanceModeMiddleware : IMiddleware
{
    private readonly bool _isUnderMaintenance;

    public MaintenanceModeMiddleware(IConfiguration config)
    {
        _isUnderMaintenance = config.GetValue<bool>("MaintenanceMode");
    }

    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        if (_isUnderMaintenance && !context.Request.Path.StartsWithSegments("/health"))
        {
            context.Response.StatusCode = 503;
            context.Response.Headers["Retry-After"] = "3600";
            context.Response.ContentType = "application/json";
            var response = JsonSerializer.Serialize(new { error = "Service temporarily unavailable" });
            await context.Response.WriteAsync(response);
            // Deliberately not calling next => short-circuit
            return;
        }

        await next(context);
    }
}
Production Insight
Short-circuiting middleware must always set the response status code and headers before writing body.
Forgetting headers like Content-Length or Retry-After causes client-side timeouts.
Also: short-circuited responses bypass downstream middleware — exception handling middleware won't catch errors from the short-circuit path.
Key Takeaway
Short-circuit = no next() call. Always set status, headers, then body.
Short-circuited paths are invisible to downstream error handlers.
Test both pass and fail paths in integration tests.

3. Middleware Ordering: The 1 Rule That Prevents 95% of Production Bugs

Middleware ordering is the single most misunderstood aspect of ASP.NET Core. The framework gives you total control, which means total responsibility. There's no built-in validation that order makes sense — you can register auth after static files and the compiler won't complain.

Here's the ordering rule most senior engineers follow, from first registered to last:

  1. Exception/error handling (to catch everything)
  2. HTTPS redirection
  3. Static files (before auth to avoid churn)
  4. Authentication
  5. Authorization (separate!)
  6. CORS
  7. Custom middleware (logging, rate limiting, etc.)
  8. Routing (app.UseRouting())
  9. Endpoints (app.UseEndpoints())
This order ensures that
  • Exceptions are caught even if they happen in auth.
  • Static files don't trigger auth checks (performance).
  • CORS runs after auth headers are set.
  • Endpoints run last, after all pipeline decisions.
Pipeline as Stack
  • Each middleware is a layer; the innermost layer is the terminal middleware (the endpoint).
  • Pre-processing happens on the way in; post-processing on the way out.
  • Short-circuiting is like peeling back to the outer layer immediately.
  • Exception handling must be the outermost layer to catch anything that happens inside.
Production Insight
CORS middleware must appear AFTER authentication but BEFORE endpoint routing. Why? Because CORS preflight requests (OPTIONS) don't include auth headers — if auth is before CORS, preflight gets rejected. This mismatch took down a public API for 2 hours in a real incident.
Common trap: people put CORS first because they think it's 'security'. Wrong. Authentication must come before CORS.
Key Takeaway
Middleware order: Exception → Https → Static → Auth → Authorize → CORS → Custom → Routing → Endpoints.
Test the ordering with a script that sends unauthenticated OPTIONS requests.

4. Custom Middleware: Write Production-Ready Components

Writing custom middleware in ASP.NET Core is straightforward, but building production-quality middleware requires handling edge cases: async disposal, logging, dependency injection lifetimes, and proper error handling.

Two patterns exist: 1. Convention-based middleware – A class with an InvokeAsync(HttpContext, RequestDelegate) method. No interface needed. Dependencies injected via constructor. 2. IMiddleware interface – Implement IMiddleware and register with builder.Services.AddSingleton<MyMiddleware>() or transient. Gives you DI lifetime control.

The convention-based approach is more common and allows constructor injection happens once at build time. That means the middleware instance is effectively singleton unless you register it differently. For scoped dependencies, inject IServiceProvider and resolve manually inside InvokeAsync.

Always handle async exceptions properly. A try/catch inside the middleware that logs and re-throws might cause double exception handling if upstream middleware also catches. Use IExceptionHandler from .NET 8+ for a clean global approach.

Avoid making middleware stateful if it's reused. Any mutable fields on a singleton middleware will be shared across requests, leading to race conditions.

io.thecodeforge.middleware.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
// TheCodeForge - Production-grade request timing middleware
// Uses ILogger, handles scoped services via IServiceProvider

public class RequestTimingMiddleware : IMiddleware
{
    private readonly ILogger<RequestTimingMiddleware> _logger;
    private readonly IServiceProvider _serviceProvider;

    public RequestTimingMiddleware(ILogger<RequestTimingMiddleware> logger, IServiceProvider serviceProvider)
    {
        _logger = logger;
        _serviceProvider = serviceProvider;
    }

    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        var stopwatch = Stopwatch.StartNew();

        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            // Log the exception but let it propagate to global handler
            _logger.LogError(ex, "Request {Path} failed after {ElapsedMs}ms", context.Request.Path, stopwatch.ElapsedMilliseconds);
            throw;
        }
        finally
        {
            stopwatch.Stop();
            // Resolve scoped metric service
            var metrics = _serviceProvider.GetRequiredService<IRequestMetrics>();
            metrics.RecordDuration(context.Request.Path, stopwatch.ElapsedMilliseconds);
        }
    }
}

// Registration in Program.cs
// builder.Services.AddTransient<RequestTimingMiddleware>();
// app.UseMiddleware<RequestTimingMiddleware>();
Production Insight
Using IMiddleware and registering as transient ensures a new instance per request. This is critical if you need scoped dependencies. But beware: transient middleware creation overhead is minimal — measured <500ns per instance.
However, if your middleware is pure (no dependencies), use convention-based for simplicity.
Key Takeaway
IMiddleware + transient registration = fresh instance per request.
Convention-based = singleton-like, careful with scoped dependencies.
Always rethrow exceptions after logging if you need global handling.

5. Performance Traps: The Hidden Cost of Ill-Ordered Middleware

Middleware pipeline overhead is usually negligible — each middleware adds <1µs when synchronous. But real problems come from async overhead and blocking calls.

Async overhead: Each await next() yields control to the thread pool, causing a continuation context switch. If a middleware does nothing async, it's better to avoid async/await entirely. Use Task.CompletedTask and synchronous code paths.

Blocking calls: Never call .Result or .Wait() on tasks inside middleware. This can cause deadlocks — especially when combined with ConfigureAwait(false) or synchronous contexts like ASP.NET Core's SynchronizationContext.

Static file middleware: By default, static file middleware runs early (before auth) to serve files quickly. But if you have many files or large files, it can still cause I/O waits. Use UseStaticFiles with appropriate caching headers and consider using CDN for offload.

Order and memory: Middleware that allocates per-request objects (e.g., memory streams, arrays) can increase GC pressure. Pool buffers using ArrayPool<byte> if you manipulate large request/response bodies.

Real production metric: A service with 15 middleware components (typical for modern APIs) adds ~12µs to each request. That's 0.012ms — insignificant. But a single middleware that does a synchronous database call? 50-200ms. The bottleneck is never the pipeline itself, it's what the middleware does.

Production Insight
A common performance bug: middleware that logs request body by reading it into a string synchronously. This blocks the thread and consumes memory. Use HttpContext.Request.BodyReader (PipeReader) for zero-copy streaming.
Another trap: middleware that calls HttpContext.Session.LoadAsync() on every request, even for static files or API endpoints that don't use sessions. That's an unnecessary database round-trip.
Key Takeaway
Pipeline overhead ~12µs for 15 middleware. The real cost is what middleware does — not the chain itself.
Avoid sync-over-async, pool buffers, and minimize per-request allocations.
Measure with Application Insights or OpenTelemetry to find the real bottleneck.
Optimize Middleware Performance
IfMiddleware does I/O (DB, file, external API)
UseUse async/await correctly; never block. Consider offloading to background service if not critical.
IfMiddleware needs request body
UseUse PipeReader for streaming. Avoid reading entire body into string for logging.
IfMany short-lived allocations per request
UseUse ObjectPool or ArrayPool to reduce GC pressure.
IfStatic file serving is slow
UseEnable caching, use CDN, reduce file count, disable directory browsing.

Use() vs Run(): Why Chaining the Wrong One Burns You in Production

You’ve seen all the middleware docs glibly toss around Use() and Run() like they’re interchangeable. They’re not. The difference isn’t academic—it’s the difference between a pipeline that short-circuits correctly and one that silently swallows exceptions at 3 AM.

Run() is a terminal delegate. It ends the pipeline. If you put a Run() in the middle of your chain, every middleware after it is dead code. I’ve debugged production outages where a junior dev dropped a Run() after authentication—cors, logging, and rate-limiting just vanished. Output was a 200 with an empty body and zero telemetry.

Use() passes the next delegate explicitly. You control the flow—call await next() or not. That’s your short-circuit switch. Use it for anything that needs to run conditionally: auth checks, correlation IDs, response compression. Only use Run() for terminal handlers that truly end the request, like a static file server or a health-check endpoint. Anything else? You’re asking for a silent production bug.

UseVsRunMiddleware.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
// io.thecodeforge — csharp tutorial

var app = WebApplication.Create(args);

app.Use(async (context, next) =>
{
    // Use() passes next — we control short-circuit
    Console.WriteLine("1: Before auth check");
    await next(); // If we omit this, pipeline ends here
    Console.WriteLine("5: After response generated");
});

app.Run(async context =>
{
    // Run() is terminal — no next exists
    Console.WriteLine("2: In Run() — body written");
    await context.Response.WriteAsync("Hello from Run()");
});

// This middleware never executes — dead code
app.Use(async (context, next) =>
{
    Console.WriteLine("NEVER REACHED");
    await next();
});

app.Run();
Output
1: Before auth check
2: In Run() — body written
5: After response generated
Production Trap:
Never place Run() before middleware that must always execute. Logging, exception handling, and CORS middleware after a Run() are dead weight—you’ll never catch the request.
Key Takeaway
Use Run() only for terminal handlers; use Use() for everything else so you own the flow.

The Pipeline Flow Diagram You'll Actually Use in Code Reviews

Most diagrams for middleware pipelines are pretty lies—neat boxes in a row with arrows. Production reality is a layered onion where one middleware wraps another. You don’t walk through sequentially; you dive in, hit next(), and bubble back out. That’s why ordering matters more than any theoretical model.

Here’s the mental model: every middleware is a function that receives the next function. The pipeline is built by nesting these lambdas at startup. When a request hits, it runs the outer wrapper, then calls inner, then returns. That’s why exception-handling middleware must be outermost: it wraps everything, so it catches exceptions thrown anywhere inside.

If you draw this as a stack with arrows going down and up, you’ll instantly spot ordering bugs. For example, if you place rate-limiting before authentication, you’re counting unauthenticated requests—a DOS hole. My rule during code review: draw the onion for every new middleware. If the arrows cross the wrong way, reject the PR.

PipelineExecutionOrder.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
// io.thecodeforge — csharp tutorial

var app = WebApplication.Create(args);

// Outer wrapper executes first on request, last on response
app.UseExceptionHandler();
app.UseAuthentication();
app.UseAuthorization();
app.Use(async (context, next) =>
{
    Console.WriteLine("Request: Check cache");
    await next(); // Inner middleware runs
    Console.WriteLine("Response: Cache result");
});
app.Run(async context =>
{
    Console.WriteLine("Terminal: Generate response");
    await context.Response.WriteAsync("Done");
});

// Execution trace:
// Request: Check cache
// Terminal: Generate response
// Response: Cache result
Output
Request: Check cache
Terminal: Generate response
Response: Cache result
Senior Shortcut:
Think of middleware as an onion: the first registered is the outermost layer. Draw it with arrows descending on request and ascending on response. Always place global handlers (errors, logging) outermost.
Key Takeaway
Middleware wraps like an onion—execution order is outermost-first on request, outermost-last on response.

Inline vs Class-Based Middleware: When to Skip the Boilerplate

You don't need a class for every middleware. Inline middleware handles simple logging, header checks, or request sanitization in 5 lines. Use app.Use() with a lambda. Zero ceremony.

But the moment you touch request state, need injected dependencies, or write branching logic, go class-based. Inline middleware hides complexity. It tempts you to chain lambdas that mutate shared state. In production, that's a ticking bomb.

Class-based middleware enforces separation. You get constructor injection for services. You get unit testability. You get a clear entry point. The rule: if your inline handler exceeds 8 lines, extract it. If it reads from HttpContext.Items, it's already too late — make a class.

The WHY is simple: inline is for filters. Class-based is for pipeline logic that matters. Don't mix them.

MiddlewareComparison.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
// io.thecodeforge — csharp tutorial

// Inline — good for simple request inspection
app.Use(async (context, next) =>
{
    Console.WriteLine($"Request: {context.Request.Path}");
    await next();
});

// Class-based — required when you need DI or state
public class RequestTimingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestTimingMiddleware> _logger;

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

    public async Task InvokeAsync(HttpContext context)
    {
        var sw = Stopwatch.StartNew();
        await _next(context);
        sw.Stop();
        _logger.LogInformation("{Path} took {Elapsed}ms", context.Request.Path, sw.ElapsedMilliseconds);
    }
}
Output
// Inline: instant setup, no testability
// Class-based: DI injection, logger, testable via mocks
Production Trap:
Never use inline middleware for anything that modifies the response body, reads request streams, or requires scoped services. You will break downstream middleware silently.
Key Takeaway
Inline middleware for filters under 8 lines. Class-based for anything that touches state or dependencies — always.

Real-World Middleware: The Patterns That Survive Code Reviews

Production middleware is boring on purpose. Three patterns cover 90% of real use: logging, authorization, and error handling.

Logging middleware captures request/response timings, request IDs, and user context. It's the first middleware in the pipeline — always. Authorization middleware short-circuits on missing tokens or bad roles. It sits right after logging, before any business logic. Error handling middleware catches unhandled exceptions, logs them, and returns a clean JSON response. It's the outermost wrapper.

Here's what kills teams: middleware that tries to do too much. A single middleware that logs, authorizes, and transforms responses is a maintenance nightmare. Split them. Each middleware has one job. If you need to share state, use HttpContext.Items sparingly and document it in a comment that will outlive the junior who wrote it.

The WHY: real-world middleware is about predictable, debuggable failures. Not clever abstractions. Not five different databases. Just clear, sequential, single-responsibility logic that you can reason about at 2 AM.

ErrorHandlingMiddleware.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
// io.thecodeforge — csharp tutorial

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

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

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (NotFoundException ex)
        {
            _logger.LogWarning(ex, "Not found: {Path}", context.Request.Path);
            context.Response.StatusCode = 404;
            await context.Response.WriteAsJsonAsync(new { error = ex.Message });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Unhandled: {Path}", context.Request.Path);
            context.Response.StatusCode = 500;
            await context.Response.WriteAsJsonAsync(new { error = "Internal server error" });
        }
    }
}
Output
// NOT_FOUND -> 404 JSON
// ANYTHING_ELSE -> 500 JSON + full stack trace logged
Senior Shortcut:
Register error middleware first, auth second, logging third. That way auth failures get caught by error handler and return consistent JSON instead of raw 401 pages.
Key Takeaway
Three middleware patterns rule production: error wrapper, auth gate, logging clock. One job each, no exceptions.

Real-World Scenarios: Middleware That Survives Production Fire

Middleware fails in production when it assumes happy paths. The gap between theory and reality shows up in three common scenarios: (1) Authentication middleware that short-circuits on expired tokens but forgets to log the failure, causing silent drops. (2) Rate-limiting middleware placed after exception handling, so throttled requests bypass your 429 response and crash upstream. (3) Localization middleware reading the wrong culture header because it runs after static file middleware has already served cached content. The fix: order middleware from broadest responsibility (exception handling, auth) to most specific (static files, endpoints). Always log short-circuits with request context. Test by throwing malformed headers, expired tokens, and rapid-fire requests. In code reviews, challenge anyone placing custom middleware after UseEndpoints() — that's a pipeline bypass waiting to burn you.

RealWorldMiddleware.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// io.thecodeforge — csharp tutorial

var app = builder.Build();

app.UseExceptionHandler(); // logs before short-circuit
app.UseAuthentication(); // rejects early, logs context
app.UseAuthorization();
app.Use(async (ctx, next) =>
{
    if (ctx.Request.Headers["X-Rate-Limit"].Count > 100)
    {
        ctx.Response.StatusCode = 429;
        await ctx.Response.WriteAsync("Throttled");
        return; // short-circuit, never reaches static files
    }
    await next();
});
app.UseStaticFiles();
app.MapControllers();
Output
No output — pipeline setup for production.
Production Trap:
Placing custom middleware after UseEndpoints() means it never executes. Middleware after the terminal handler is dead code.
Key Takeaway
Order middleware by failure scope: exception handling first, then auth, rate-limiting, static files, endpoints.

Interview-Ready Summary: Master the Pipeline in 3 Minutes

When an interviewer asks about middleware, they want proof you understand the request lifecycle, not a definition. State these three things: (1) The pipeline is a chain of delegates where each middleware calls the next or short-circuits. Use() chains, Run() terminates. (2) Order determines behavior: exception handling must go first, auth before endpoints, static files before MVC. Wrong order causes silent security holes or 500s that bypass logging. (3) Common interview trap: middleware execution resumes after await next() only if the middleware doesn't short-circuit. So logging middleware must call next() and then log — that's dual-phase processing. For code reviews, always ask: 'Does this middleware short-circuit? Where does its response go?' That question alone reveals ordering bugs. Production rule: if you can't explain why it's placed there, it's in the wrong spot.

InterviewSummary.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// io.thecodeforge — csharp tutorial

public class LoggingMiddleware
{
    private readonly RequestDelegate _next;
    public LoggingMiddleware(RequestDelegate next) => _next = next;

    public async Task InvokeAsync(HttpContext ctx)
    {
        var stopwatch = Stopwatch.StartNew();
        await _next(ctx); // dual-phase: before and after
        stopwatch.Stop();
        Console.WriteLine($"{ctx.Request.Path} took {stopwatch.ElapsedMilliseconds}ms");
    }
}

// Middleware order: exception -> logging -> auth -> endpoints
Output
GET /api/orders took 45ms
Interview Shortcut:
If asked 'where does logging go?', answer: after exception handler, before auth. That shows you prioritize visibility over security order.
Key Takeaway
Master the pipeline by understanding short-circuit, dual-phase, and ordering — not just Use() vs Run().
● Production incidentPOST-MORTEMseverity: high

The Silent Auth Bypass That Took Down a Payment API

Symptom
Production logs showed successful 500 responses without any authentication claims on the HttpContext. Error responses included full SQL connection strings and stack traces.
Assumption
Team assumed authentication middleware would always run before any middleware that produces a response. The exception handler middleware was registered first in Program.cs.
Root cause
Exception handling middleware was registered before authentication middleware. When an unhandled exception occurred downstream, the exception handler caught it and wrote a response before any authentication middleware had a chance to run. The result: unauthenticated users received detailed error pages.
Fix
Moved exception handling middleware to after authentication and authorization middleware in the pipeline. Used IStartupFilter to enforce ordering conventions across team members.
Key lesson
  • Always register middleware that handles errors AFTER security middleware (auth, CORS, HTTPS redirection).
  • Use a custom middleware ordering checklist in your team's PR template.
  • Test pipeline ordering by sending unauthenticated requests to endpoints that throw exceptions.
Production debug guideDiagnose ordering issues and unexpected behavior step by step4 entries
Symptom · 01
Authentication works in development but fails in production
Fix
Check if any middleware is short-circuiting the pipeline before the auth middleware runs. Common culprit: static file middleware registered before app.UseAuthentication().
Symptom · 02
CORS errors despite registering CORS middleware
Fix
Verify CORS middleware is registered before endpoint routing. Also ensure the CORS policy is not being overridden by a downstream middleware that clears response headers.
Symptom · 03
Exception responses contain sensitive data in production
Fix
Check the order of exception handling middleware. It must be registered after authentication and authorization but before developer exception page in non-development environments.
Symptom · 04
Middleware not executing for certain requests
Fix
Test with a simple middleware that writes to console. If it doesn't run, something upstream is short-circuiting. Use app.Use() to log each request path before all other middleware.
★ Quick Debug Cheatsheet: Middleware Pipeline IssuesCommon symptoms and immediate actions for pipeline problems
Middleware never runs
Immediate action
Check if a previous middleware called `context.Response.WriteAsync()` without calling `next`.
Commands
dotnet run --environment Development
Add a diagnostic middleware at the very top that logs each request path.
Fix now
Ensure your middleware calls await next() unless intentionally short-circuiting.
Response headers missing (e.g., CORS, Security headers)+
Immediate action
Check if a middleware is clearing response headers after they were set.
Commands
Use curl -I to inspect response headers: curl -I https://localhost:5001/
Add a middleware to log response headers before and after each middleware call.
Fix now
Move header-setting middleware to after all middleware that might modify headers, or use OnStarting callback to set headers late.
Exceptions become generic 500 with no details+
Immediate action
Check if developer exception page middleware is present but only in Development.
Commands
Check appsettings.Production.json for `ASPNETCORE_ENVIRONMENT` setting.
Look for `app.UseDeveloperExceptionPage()` placed after `if (env.IsDevelopment())` block.
Fix now
Add exception handling middleware that logs detailed errors while returning sanitized responses. Use app.UseExceptionHandler("/error").
Middleware Registration Patterns Comparison
AspectConvention-basedIMiddleware
Registration styleapp.UseMiddleware<MyMiddleware>() or app.Use() delegateRequires service registration then app.UseMiddleware<MyMiddleware>()
Dependency injection lifetimeConstructor injected once (effectively singleton). Use IServiceProvider for per-request services.Controlled by service lifetime (transient, scoped, singleton)
Instance creationCreated once if middleware is stateless – or per request if registered transient via IMiddlewareFactoryCreated per request if registered as transient
Async supportInvokeAsync must return TaskInvokeAsync must return Task
TestabilityHarder to unit test because dependencies are resolved at startupEasy to test: can inject mocked dependencies
Performance overheadSlightly faster (no DI resolution per request)Slightly slower (DI resolution per request) but negligible

Key takeaways

1
The middleware pipeline is a linked list of request delegates chained at startup.
2
Order determines execution
exception → auth → CORS → custom → routing → endpoints.
3
Short-circuiting must produce complete response and bypass downstream.
4
Custom middleware
convention-based for stateless, IMiddleware for scoped dependencies.
5
Pipeline overhead is tiny
profile the actual work inside middleware, not the chain itself.
6
Test pipeline ordering with integration tests that send unauthenticated and OPTIONS requests.

Common mistakes to avoid

5 patterns
×

Registering exception handling middleware inside `if (env.IsDevelopment())` block in production

Symptom
Production servers still show detailed exception pages with stack traces to end users when errors occur.
Fix
Always register exception handling middleware unconditionally. Use if (env.IsDevelopment()) app.UseDeveloperExceptionPage(); else app.UseExceptionHandler("/error");
×

Putting CORS middleware before authentication

Symptom
CORS preflight requests (OPTIONS) are rejected with 401 because they lack authentication headers.
Fix
Register CORS AFTER authentication. For preflight, authentication middleware should ignore OPTIONS requests.
×

Using singleton middleware with scoped DbContext

Symptom
Data staleness: DbContext is captured at startup and never refreshed. Multiple requests share the same context leading to concurrency issues.
Fix
Inject IServiceProvider and resolve DbContext inside InvokeAsync, or register middleware as transient with IMiddleware.
×

Forgetting to call `next` in short-circuit paths

Symptom
Some requests never reach the endpoint, but no response is produced either — client hangs forever.
Fix
Always ensure every code path either calls next or produces a complete response (status + headers + body). Use return; after writing response.
×

Modifying `HttpContext.Request.Body` without enabling buffering

Symptom
Reading the request body twice throws InvalidOperationException because stream is forward-only.
Fix
Enable request buffering: context.Request.EnableBuffering(); then reset position before re-reading.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain how the ASP.NET Core middleware pipeline is constructed under th...
Q02SENIOR
You have a middleware that catches exceptions and returns a JSON error r...
Q03JUNIOR
What is the difference between `app.Use()` and `app.Run()` and when woul...
Q04SENIOR
Your team's API is suddenly returning 401 for preflight CORS requests ev...
Q01 of 04SENIOR

Explain how the ASP.NET Core middleware pipeline is constructed under the hood. Include the role of `IApplicationBuilder` and how closures are used.

ANSWER
The pipeline is built by chaining Func<RequestDelegate, RequestDelegate> delegates. Each call to Use() or UseMiddleware<T>() adds a wrapper. IApplicationBuilder holds a list of these middleware delegates. When Build() is called, it composes them into a single RequestDelegate by nesting them: the innermost middleware (first registered) wraps the terminal middleware (app.Run). Each middleware's InvokeAsync receives the next delegate as a closure. At runtime, each middleware can process before, after, or short-circuit.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the middleware pipeline in ASP.NET Core?
02
How do I debug middleware order issues?
03
Can middleware be added conditionally?
04
What is the performance impact of many middleware?
05
How do I write middleware that depends on scoped services?
N
Naren Founder & Principal Engineer

20+ years shipping production .NET services in enterprise systems. Everything here is grounded in real deployments.

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

That's C# Advanced. Mark it forged?

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

Previous
IDisposable and using Statement
12 / 15 · C# Advanced
Next
Source Generators in C#