Senior 5 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
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
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
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.

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.
● 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?
🔥

That's C# Advanced. Mark it forged?

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

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