Skip to main content

Command Palette

Search for a command to run...

7 Common ASP.NET Core Middleware Mistakes and How to Fix Them

Updated
โ€ข11 min read
7 Common ASP.NET Core Middleware Mistakes and How to Fix Them

Middleware is one of the most powerful abstractions in ASP.NET Core โ€” and one of the most misunderstood. Every request and response passes through your middleware pipeline, which means a single misconfigured component can silently break authentication, corrupt responses, introduce memory leaks, or tank performance under load. These are the kinds of bugs that don't show up in unit tests and only reveal themselves in production.

Most developers learn the basics โ€” register a middleware, call await next(context) โ€” and move on. But the gaps between "it compiles" and "it behaves correctly in production" are where teams lose hours of debugging time. The full working implementations for common middleware patterns, including idempotency, timeout handling, and request context enrichment, are available on Patreon โ€” annotated, production-ready, and mapped to real enterprise scenarios.

If you want to see how these patterns connect inside a complete ASP.NET Core API โ€” from the pipeline configuration all the way through to error handling and observability โ€” Chapter 1 and Chapter 6 of the Zero to Production course walk through exactly that, with a working codebase you can run immediately.

Mistake 1: Getting the Pipeline Order Wrong

The most common โ€” and most damaging โ€” middleware mistake is registering components in the wrong order. ASP.NET Core processes middleware sequentially: on the way in, components execute in registration order; on the way out, they execute in reverse. Swap two lines and you can silently break your entire authentication flow.

The canonical ordering mistake is placing UseAuthorization() before UseAuthentication(). Authorization checks happen before the identity is established, so every request arrives at your authorization logic as anonymous โ€” and depending on your policy configuration, either everything passes or everything fails, with no error to tell you why.

The correct pipeline order for a production ASP.NET Core application:

  1. UseExceptionHandler() or UseDeveloperExceptionPage() โ€” must be first to catch exceptions from all downstream components
  2. UseHsts() / UseHttpsRedirection() โ€” transport-level concerns before any processing
  3. UseStaticFiles() โ€” serve static assets before the routing overhead kicks in
  4. UseRouting() โ€” establish route context before auth middleware needs it
  5. UseCors() โ€” CORS must run after routing so it has endpoint metadata, but before authentication
  6. UseAuthentication() โ€” establish identity before making authorization decisions
  7. UseAuthorization() โ€” evaluate policies after identity is known
  8. UseRateLimiter() โ€” rate limiting after auth so you can partition by user identity
  9. Custom business middleware โ€” runs with full context available
  10. MapControllers() / MapEndpoints() โ€” terminal endpoint execution

Deviating from this order without a deliberate reason is the single most reliable way to introduce authentication bugs that are invisible in development and catastrophic in production.

Mistake 2: Resolving Scoped Services in Middleware Constructors

Middleware components are registered as singletons in the ASP.NET Core container โ€” they are created once and reused for the lifetime of the application. This creates a well-known but frequently ignored problem: if you inject a scoped service (like a DbContext or a repository) directly into a middleware constructor, you are capturing a scoped service in a singleton scope.

The result is a captive dependency: the scoped service lives as long as the singleton, which means it is shared across requests. In the case of EF Core, this means a single DbContext instance handling concurrent requests โ€” leading to thread-safety violations, stale query results, and data corruption.

The fix is to use IServiceScopeFactory and create a fresh scope inside InvokeAsync, not in the constructor. The constructor should only receive singletons: ILogger<T>, IOptions<T>, and IHttpClientFactory are all safe. Anything with a scoped or transient lifetime needs to be resolved per-request inside the InvokeAsync method body.

This is one of the patterns where seeing the correct structure alongside the broken one makes the distinction clear โ€” both versions live on Patreon with annotations explaining exactly why each approach behaves the way it does.

Mistake 3: Not Calling await next(context) โ€” or Calling It After Writing the Response

Middleware that forgets to call next(context) silently short-circuits the entire pipeline. Every component registered after it โ€” including routing, authentication, and your controllers โ€” is skipped. The request returns a blank response or whatever the middleware wrote, with no error logged.

The opposite problem is equally dangerous: writing to the response body and then calling await next(context). Once you start writing to HttpResponse.Body, the response headers are sent and the status code is locked. Any downstream middleware that tries to modify the response will either throw an exception or produce a corrupted response.

The rule is straightforward:

  • If your middleware is a pass-through (logging, enrichment, correlation ID injection), call await next(context) and only touch the response on the return path if the headers have not yet been sent โ€” check context.Response.HasStarted before writing.
  • If your middleware is terminal (it fully handles the request), write the response and return without calling next. Use app.Run() instead of app.Use() to make this intent explicit in code.

Mistake 4: Registering the Same Middleware Twice

This is subtle and easy to overlook in larger applications. If you register the same middleware component twice โ€” either by calling app.UseMiddleware<MyMiddleware>() twice, or by having both an extension method call and a direct UseMiddleware call in different parts of Program.cs โ€” the component runs twice per request.

For logging and correlation ID middleware, double registration typically means duplicate log entries or double-written headers. For authentication middleware, it can mean double token validation, adding latency on every request. For CORS middleware, double registration is a known source of duplicate Access-Control-Allow-Origin headers, which violates the CORS specification and causes browsers to reject responses.

The fix is auditing your pipeline at startup โ€” or better, using integration tests that validate the number of times a specific header is set or a specific log entry is written per request. For middleware that must run exactly once, use IStartupFilter or a flag in IHttpContextAccessor to enforce single execution.

Mistake 5: Blocking the Thread Inside Middleware

ASP.NET Core is built on a non-blocking async model. Every component in the pipeline is expected to be async โ€” not just to return a Task, but to genuinely await I/O without blocking the calling thread.

The most common violation is calling .Result or .Wait() on an async operation inside InvokeAsync. This blocks the thread-pool thread while waiting for I/O to complete, which defeats the scalability model of the entire framework. Under load, this can starve the thread pool and cause request queuing that looks exactly like a CPU bottleneck when profiling.

Less obvious is the Task.Run() antipattern: wrapping blocking I/O in Task.Run() does offload the blocking work from the request thread, but it consumes a thread-pool thread anyway. For network I/O and database calls, always use await on genuinely async methods โ€” HttpClient.SendAsync(), DbContext.ToListAsync(), Redis.GetAsync(). If you are calling into a third-party library that only exposes synchronous methods, that is a library-level problem โ€” consider wrapping it in a dedicated BackgroundService channel pattern rather than blocking the request thread.

Mistake 6: Putting Business Logic Directly in Middleware

Middleware exists to handle cross-cutting concerns: authentication, logging, correlation IDs, response compression, CORS, rate limiting. It is not the right home for business logic.

Business logic in middleware is harder to test, harder to reuse, and tightly coupled to the HTTP pipeline. If you want to validate an API key, verify a tenant ID, or check a feature flag, the temptation is to reach for a custom middleware โ€” but the correct solution is an IAuthorizationHandler (for access control), an action filter (for request-scoped validation), or a MediatR pipeline behaviour (for application-layer concerns).

The distinction matters in practice: filters and MediatR behaviours are instantiated per-request with full DI support and are trivially unit-testable in isolation. Middleware runs for every request including static files, health checks, and OPTIONS preflight calls โ€” scoping it incorrectly means your logic runs on traffic you never intended it to reach.

Custom middleware is the right choice when you genuinely need to operate at the HTTP pipeline level, before routing resolves an endpoint. For everything else, use the abstraction closest to where the concern actually lives.

Mistake 7: Using IMiddleware and UseMiddleware<T>() Inconsistently

ASP.NET Core offers two patterns for writing custom middleware: the convention-based approach using UseMiddleware<T>() with a constructor that accepts RequestDelegate, and the interface-based approach using IMiddleware with UseMiddleware<T>() or the IMiddlewareFactory.

The critical difference is lifetime. Convention-based middleware is instantiated once as a singleton, regardless of how the type is registered in DI. IMiddleware is activated per-request by the IMiddlewareFactory, which means it is resolved from the DI container on every request and can safely be registered as scoped or transient.

Teams that mix both approaches in the same application often end up with inconsistent scoping behaviour that is difficult to reason about. Worse, developers who register a convention-based middleware type in DI as scoped or transient โ€” expecting per-request activation โ€” are surprised to find it still behaves as a singleton because the convention-based factory ignores the DI registration and always resolves from the constructor.

The recommendation for modern .NET 10 applications: prefer IMiddleware for any middleware that needs to interact with scoped services. It eliminates the captive dependency problem described in Mistake 2 and makes the middleware's lifetime explicit and consistent with the rest of your DI registrations.


For developers building production APIs, understanding these failure modes is what separates middleware that works in the happy path from middleware that holds up under real traffic and edge cases. If you want to explore a complete idempotency middleware implementation โ€” with request deduplication, distributed cache integration, and full test coverage โ€” the source code is on GitHub.

โ˜• If this helped you catch a bug before it hit production, consider buying us a coffee โ€” it keeps the deep-dives coming.

Frequently Asked Questions

Does the order of middleware registration matter in ASP.NET Core? Yes โ€” pipeline order is one of the most impactful configuration decisions in an ASP.NET Core application. Middleware executes in registration order on the inbound path and in reverse on the outbound path. Placing UseAuthorization() before UseAuthentication(), or UseCors() before UseRouting(), are common ordering mistakes that produce silent failures rather than obvious errors.

Can I inject scoped services into a custom middleware class? Not safely via the constructor. Middleware is a singleton, so constructor-injected scoped services become captive dependencies shared across requests. Instead, accept IServiceScopeFactory in the constructor and resolve scoped services inside InvokeAsync using factory.CreateScope(). The IMiddleware interface avoids this entirely because it is activated per-request by the DI container.

What is the difference between IMiddleware and convention-based middleware in ASP.NET Core? Convention-based middleware (the class with a RequestDelegate constructor parameter and an InvokeAsync method) is instantiated once as a singleton by the framework. IMiddleware is resolved per-request by the IMiddlewareFactory, meaning it participates fully in the DI container's lifetime management. For middleware that needs scoped services or per-request state, IMiddleware is the safer choice in .NET 10 applications.

How do I prevent my middleware from running on health check and OPTIONS requests? Use UseWhen() or MapWhen() to conditionally branch the pipeline. These extension methods let you apply middleware only when a predicate is true โ€” for example, only on non-GET requests, only on paths under /api/, or only when a specific header is present. This avoids running business middleware on infrastructure traffic like /health/live, /health/ready, or CORS preflight requests.

What happens if I write to the response and then call await next(context)? Once you begin writing to HttpResponse.Body, the response headers are flushed and the status code is committed. Calling await next(context) after writing means downstream middleware receives an already-started response. If downstream middleware attempts to set headers or change the status code, it will either throw an InvalidOperationException or silently fail. Always check context.Response.HasStarted before writing on the outbound path, and never call next after starting a response.

Is it acceptable to put feature flag checks in middleware? Feature flags for routing or access control can legitimately live in middleware when they need to run before endpoint resolution. However, feature flags that control business behaviour โ€” which variant of a service to use, which pricing tier applies โ€” belong in application-layer services, not middleware. Keeping feature flag evaluation close to the business logic that uses it makes the code easier to test and easier to reason about.

How do I debug unexpected behaviour in my ASP.NET Core middleware pipeline? The most reliable technique is adding MiddlewareAnalysis (available via the Microsoft.AspNetCore.MiddlewareAnalysis package) in your development environment โ€” it logs every middleware component that executes per request, in order. Pairing this with structured logging in each custom middleware component (log the entry and exit of InvokeAsync) gives you a clear trace of what ran, in what order, and how long each component took.