top of page

Middleware and Pipelines in .NET: Understanding the Request Flow

  • ShiftQuality Contributor
  • Jan 14
  • 5 min read

The previous posts in this path covered dependency injection and Entity Framework Core. This post covers the infrastructure that ties everything together in an ASP.NET Core application: the middleware pipeline — the sequence of components that process every HTTP request from arrival to response.

When a request hits your ASP.NET Core application, it does not go directly to your controller or endpoint. It flows through a pipeline of middleware components, each of which can inspect the request, modify it, pass it along, or short-circuit the pipeline entirely. Authentication, logging, error handling, CORS, compression — these are all middleware, and the order they run in matters more than most developers realize.

The Pipeline Mental Model

Think of the middleware pipeline as a series of nested layers, like an onion. A request enters from the outside, passes through each layer going in, reaches the core (your endpoint), and then the response passes back through each layer going out.

Each middleware component has two chances to act: once on the way in (before the next middleware runs) and once on the way out (after the next middleware has finished). This is why middleware order matters — authentication must run before authorization, which must run before your endpoint. On the way out, response compression should run after your endpoint has produced the response body.

The key insight: if a middleware does not call next(), the pipeline stops. The request never reaches the remaining middleware or the endpoint. This is called short-circuiting, and it is how authentication middleware rejects unauthenticated requests — it returns a 401 without ever calling the next middleware in the chain.

The Order Matters

ASP.NET Core middleware runs in the exact order you register it in Program.cs. This is not alphabetical, not dependency-based — it is the literal order of your app.Use* calls.

The conventional order exists for good reasons. Exception handling middleware goes first (outermost layer) so it can catch exceptions from any middleware further down the pipeline. HTTPS redirection goes early to ensure all subsequent processing happens over a secure connection. Static file middleware goes before routing so that requests for CSS, JavaScript, and images are served quickly without hitting the routing system. Authentication goes before authorization — you must know who the user is before you can check what they are allowed to do. Authorization goes before endpoint execution.

Getting this order wrong produces subtle bugs. Put authentication after your endpoint and requests are handled before identity is established. Put exception handling in the middle and exceptions from earlier middleware are unhandled. Put CORS middleware after routing and preflight requests fail. These bugs are hard to diagnose because the behavior changes based on the specific request — some requests work fine while others fail in unexpected ways.

Writing Custom Middleware

Custom middleware handles cross-cutting concerns — logic that applies to many or all requests but does not belong in individual endpoints. Request logging, performance timing, tenant identification in multi-tenant applications, custom header injection, and request rate limiting are all middleware candidates.

The simplest approach is inline middleware using app.Use:

app.Use(async (context, next) =>
{
    var stopwatch = Stopwatch.StartNew();
    await next(context);
    stopwatch.Stop();
    context.Response.Headers["X-Response-Time"] =
        $"{stopwatch.ElapsedMilliseconds}ms";
});

This middleware measures how long the rest of the pipeline takes to process the request and adds the timing to a response header. It acts on both the way in (starts the timer) and the way out (records the elapsed time).

For more complex middleware, create a class with an InvokeAsync method. The class receives the RequestDelegate (the next middleware) in its constructor and the HttpContext in its InvokeAsync method. This pattern supports dependency injection — the middleware class can take services from the DI container in its constructor.

The rule for whether logic belongs in middleware or in the endpoint: if the logic applies to all (or most) requests regardless of the endpoint, it is middleware. If the logic is specific to one endpoint or a small group of endpoints, it belongs in the endpoint (or an action filter).

Exception Handling Middleware

The built-in UseExceptionHandler middleware catches unhandled exceptions and converts them to HTTP responses. But the default behavior — returning a generic error page or a 500 with no body — is rarely what you want in production.

Custom exception handling middleware lets you control the error response format, log the exception with context, and return structured error responses that clients can parse. For an API, this might mean returning a JSON error object with an error code, a human-readable message, and a correlation ID for support requests.

The pattern: register exception handling middleware as the outermost layer (first in the pipeline). When any downstream middleware or endpoint throws, the exception propagates up to the handler. The handler logs the exception with full context — request path, query parameters, user identity, correlation ID — and returns an appropriate HTTP response without leaking internal details (stack traces, connection strings) to the client.

The distinction between development and production exception handling is important. In development, you want detailed error pages with stack traces and request details (UseDeveloperExceptionPage). In production, you want structured error responses that give the client enough information to report the problem without revealing internal architecture.

Middleware vs. Filters

ASP.NET Core also has filters — action filters, authorization filters, result filters, exception filters. Filters run within the MVC/endpoint routing pipeline, after routing has determined which endpoint handles the request. Middleware runs before routing.

The distinction: middleware sees every request. Filters see only routed requests that match an endpoint. Middleware can short-circuit before routing happens. Filters can access controller-specific context (model binding, action arguments) that middleware cannot.

Use middleware for concerns that apply globally and do not need endpoint context — logging, CORS, authentication, static files. Use filters for concerns that apply to specific endpoints or need access to the endpoint's context — model validation, endpoint-specific authorization rules, response caching.

The common mistake: implementing something as middleware that should be a filter, or vice versa. A middleware that tries to access the current user's roles before authentication middleware has run will find no user. A filter that tries to handle requests for static files will never fire because static file middleware serves them before routing.

Performance Considerations

Every middleware component adds processing time to every request. A middleware that does a database query on every request adds latency to every request — including static file requests, health checks, and requests that do not need the database result.

Be mindful of what each middleware does and whether it needs to run for all requests. Use MapWhen or UseWhen to conditionally apply middleware — run tenant identification middleware only for API routes, not for static files. Use Map to branch the pipeline for different URL prefixes, applying different middleware stacks to different parts of the application.

Asynchronous middleware should not block. If your middleware calls an external service, use await and do not wrap synchronous blocking calls in Task.Run. Blocking a thread pool thread in middleware under high load can exhaust the thread pool and make the entire application unresponsive.

The Takeaway

The middleware pipeline is the backbone of request processing in ASP.NET Core. Every request flows through the pipeline in order, and the order determines behavior. Understanding the pipeline — what runs when, why the order matters, and when to write custom middleware versus filters — is foundational knowledge for building reliable .NET web applications.

The pipeline is not magic. It is a sequence of functions that each get a chance to inspect, modify, or short-circuit the request. When you understand that sequence, debugging request-handling issues becomes straightforward — you know exactly which layer to investigate and what it should have done.

Next in the "Building Real .NET Apps" learning path: We'll cover configuration and secrets management in .NET — how to handle environment-specific settings without hardcoding values or leaking credentials.

Comments


bottom of page