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.
thecodeforge.io
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 WebApplicationbuilder 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.
// TheCodeForge - Middleware pipeline construction example// Demonstrates how Use() chains delegates and how Build() creates final RequestDelegatevar 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.
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.
thecodeforge.io
Middleware Pipeline Dotnet
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:
Think of middleware pipeline as a stack of onions — requests pass inward through layers, responses pass outward in reverse order.
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.
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.
// TheCodeForge - Production-grade request timing middleware// Uses ILogger, handles scoped services via IServiceProviderpublicclassRequestTimingMiddleware : IMiddleware
{
privatereadonlyILogger<RequestTimingMiddleware> _logger;
privatereadonlyIServiceProvider _serviceProvider;
publicRequestTimingMiddleware(ILogger<RequestTimingMiddleware> logger, IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
}
publicasyncTaskInvokeAsync(HttpContext context, RequestDelegate next)
{
var stopwatch = Stopwatch.StartNew();
try
{
awaitnext(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 servicevar 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 tutorialvar app = WebApplication.Create(args);
app.Use(async (context, next) =>
{
// Use() passes next — we control short-circuitConsole.WriteLine("1: Before auth check");
await next(); // If we omit this, pipeline ends hereConsole.WriteLine("5: After response generated");
});
app.Run(async context =>
{
// Run() is terminal — no next existsConsole.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");
awaitnext();
});
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.
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}");
awaitnext();
});
// Class-based — required when you need DI or statepublicclassRequestTimingMiddleware
{
privatereadonlyRequestDelegate _next;
privatereadonlyILogger<RequestTimingMiddleware> _logger;
publicRequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
{
_next = next;
_logger = logger;
}
publicasyncTaskInvokeAsync(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.
// 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.
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 tutorialpublicclassLoggingMiddleware
{
privatereadonlyRequestDelegate _next;
publicLoggingMiddleware(RequestDelegate next) => _next = next;
publicasyncTaskInvokeAsync(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.
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.
Q02 of 04SENIOR
You have a middleware that catches exceptions and returns a JSON error response. However, you notice that when an exception is thrown in a downstream middleware, the response status is always 200 instead of 500. What's wrong?
ANSWER
The exception-handling middleware likely sets the response status code to 200 instead of preserving the original error. The fix: inside the catch block, set context.Response.StatusCode = 500; before writing the body. Also ensure that you're not accidentally swallowing the exception without setting the correct status code. Use the ExceptionHandlerOptions or IExceptionHandler interface to simplify.
Q03 of 04JUNIOR
What is the difference between `app.Use()` and `app.Run()` and when would you use each?
ANSWER
app.Use() adds a middleware that can call next to continue the pipeline. It's for middleware that processes both before and after downstream components. app.Run() adds a terminal middleware — it does not call next and terminates the pipeline. Use Run for the last middleware that produces a response. Use for all other middleware. You can also use Use for short-circuiting by not calling next.
Q04 of 04SENIOR
Your team's API is suddenly returning 401 for preflight CORS requests even though CORS is configured correctly in `Startup.cs`. What's the most likely cause and how do you fix it?
ANSWER
Most likely the authentication middleware is rejecting the OPTIONS request before CORS middleware runs. CORS middleware must be registered AFTER authentication. However, authentication middleware should not reject OPTIONS requests. Fix: in your authentication middleware, check if the request method is OPTIONS and if so, call next without applying authentication. Alternatively, move CORS before authentication, but that's not recommended because response headers might be overwritten. The correct order: Auth middleware that skips OPTIONS, then CORS middleware.
01
Explain how the ASP.NET Core middleware pipeline is constructed under the hood. Include the role of `IApplicationBuilder` and how closures are used.
SENIOR
02
You have a middleware that catches exceptions and returns a JSON error response. However, you notice that when an exception is thrown in a downstream middleware, the response status is always 200 instead of 500. What's wrong?
SENIOR
03
What is the difference between `app.Use()` and `app.Run()` and when would you use each?
JUNIOR
04
Your team's API is suddenly returning 401 for preflight CORS requests even though CORS is configured correctly in `Startup.cs`. What's the most likely cause and how do you fix it?
SENIOR
FAQ · 5 QUESTIONS
Frequently Asked Questions
01
What is the middleware pipeline in ASP.NET Core?
It's a chain of components that process HTTP requests and responses. Each middleware can inspect, modify, or short-circuit the pipeline. The pipeline is built at startup and frozen.
Was this helpful?
02
How do I debug middleware order issues?
Add a simple logging middleware at the top of the pipeline that writes Before: {path} and After: {status}. This shows the actual execution order.
Was this helpful?
03
Can middleware be added conditionally?
Yes, using app.UseWhen() or custom IStartupFilter to conditionally include middleware based on request path or environment.
Was this helpful?
04
What is the performance impact of many middleware?
Each middleware adds <1µs overhead if synchronous and non-blocking. The real cost is in the operations they perform (I/O, DB, serialization). Profile with Stopwatch or Application Insights.
Was this helpful?
05
How do I write middleware that depends on scoped services?
Implement IMiddleware and register it as transient. In constructor, inject IServiceProvider or use IMiddlewareFactory to control instance creation.