DEV_NET_CORE
GET_STARTED
.NETDependency injection, configuration, middleware, and logging

Middleware Ordering and Cross-Cutting Behavior

Overview

Middleware ordering is the way an ASP.NET Core application arranges request-processing components in the HTTP pipeline. Each middleware can inspect the incoming request, perform work before the next middleware runs, call the next middleware, perform work after the next middleware returns, or stop the request from going further.

Cross-cutting behavior means behavior that applies across many endpoints instead of belonging to one specific controller, Minimal API handler, Razor Page, or service method. Examples include exception handling, logging, authentication, authorization, CORS, rate limiting, response compression, localization, request timing, correlation IDs, and security headers.

This topic matters because ASP.NET Core middleware is order-sensitive. A middleware placed too early, too late, or after a terminal middleware may not run at all or may produce incorrect security and runtime behavior. For example, authorization must run after authentication, CORS must be placed where it can apply headers correctly, exception handling must be early enough to catch downstream failures, and endpoint execution must happen after routing has selected the endpoint.

Middleware ordering is important in interviews because it tests whether a developer understands the ASP.NET Core request pipeline beyond writing controllers. Interviewers often ask this topic to evaluate practical production knowledge: how requests flow, where to place common middleware, how cross-cutting concerns should be centralized, how to avoid duplicated logic, and how to debug problems caused by incorrect ordering.

Core Concepts

What Middleware Is

Middleware is software assembled into a request pipeline. In ASP.NET Core, a request enters the pipeline, moves through middleware components in the order they are registered, reaches an endpoint or terminal component, and then the response travels back through earlier middleware in reverse order.

A middleware can do three main things:

  • Run logic before the next middleware.
  • Call the next middleware by invoking next.
  • Run logic after the next middleware returns.

Example inline middleware:

Code
app.Use(async (context, next) =>
{
    Console.WriteLine("Before next middleware");

    await next(context);

    Console.WriteLine("After next middleware");
});

For a request, the Before logic runs in registration order. The After logic runs in reverse order as the response returns through the pipeline.

Middleware Pipeline Flow

A simplified flow looks like this:

Code
Request
  -> Middleware 1 before next
    -> Middleware 2 before next
      -> Endpoint or terminal middleware
    <- Middleware 2 after next
  <- Middleware 1 after next
Response

This explains why middleware ordering affects both requests and responses. A logging middleware placed early can time almost the whole request. A response-header middleware placed after a terminal middleware may never run. A middleware that tries to modify headers after the response has started can fail.

Use, Run, Map, and UseWhen

ASP.NET Core provides several common ways to build or branch the pipeline.

Use adds middleware that normally receives a next delegate:

Code
app.Use(async (context, next) =>
{
    await next(context);
});

Run adds terminal middleware. It does not receive a next delegate and ends the pipeline for matching requests:

Code
app.Run(async context =>
{
    await context.Response.WriteAsync("Handled here");
});

Any middleware registered after a terminal Run for the same path will not execute.

Map creates a branch based on the request path:

Code
app.Map("/health", healthApp =>
{
    healthApp.Run(async context =>
    {
        await context.Response.WriteAsync("OK");
    });
});

UseWhen creates a conditional branch that can rejoin the main pipeline when the branch does not end the request:

Code
app.UseWhen(
    context => context.Request.Path.StartsWithSegments("/api"),
    apiBranch =>
    {
        apiBranch.Use(async (context, next) =>
        {
            context.Response.Headers.TryAdd("X-API-Branch", "true");
            await next(context);
        });
    });

Terminal Middleware and Short-Circuiting

Short-circuiting means a middleware handles the request and does not call the next middleware. This is useful when further processing is unnecessary.

Common examples include:

  • Static file middleware serving a file and stopping the pipeline.
  • Authentication middleware handling an external login callback.
  • Rate limiting middleware rejecting a request.
  • A custom maintenance-mode middleware returning 503 Service Unavailable.
  • A health-check endpoint returning a response immediately.

Example custom short-circuit:

Code
app.Use(async (context, next) =>
{
    if (context.Request.Path.StartsWithSegments("/maintenance"))
    {
        context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
        await context.Response.WriteAsync("Service temporarily unavailable.");
        return;
    }

    await next(context);
});

Short-circuiting can improve performance, but it can also accidentally bypass logging, authentication, authorization, CORS, or headers if placed incorrectly.

Cross-Cutting Behavior

Cross-cutting behavior is behavior that should apply consistently across many requests.

Common examples:

Cross-cutting concernTypical middleware responsibility
Exception handlingConvert unhandled exceptions into safe error responses
LoggingRecord request start, end, duration, status code, and errors
Correlation IDsAttach a request ID to logs and responses
AuthenticationIdentify the user
AuthorizationCheck whether the user can access a resource
CORSApply cross-origin rules for browser clients
Rate limitingReject or delay excessive requests
LocalizationSet request culture
Response compressionCompress supported responses
Security headersAdd headers such as HSTS or content security policies

Middleware is a good place for cross-cutting behavior when the concern operates at the HTTP request/response level. For business rules that depend on use-case logic, application services, MediatR pipeline behaviors, filters, or domain services may be a better fit.

Common Middleware Ordering in ASP.NET Core

A typical order for many ASP.NET Core MVC, Razor Pages, or Minimal API applications looks like this:

Code
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthentication();
builder.Services.AddAuthorization();
builder.Services.AddCors();
builder.Services.AddRateLimiter(_ => { });
builder.Services.AddControllers();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseCors("DefaultPolicy");

app.UseAuthentication();
app.UseAuthorization();

app.UseRateLimiter();

app.MapControllers();

app.Run();

The exact order can vary by application, but several rules are common:

  • Exception handling should be early so it can catch exceptions from downstream middleware and endpoints.
  • HTTPS redirection and HSTS are security-related and usually belong early.
  • Static files can run early so static asset requests do not need unnecessary endpoint processing.
  • Routing must run before middleware that needs endpoint metadata.
  • CORS often belongs after routing and before authentication/authorization and endpoints.
  • Authentication must run before authorization.
  • Authorization must run before endpoint execution.
  • Endpoint mappings such as MapControllers, MapGet, and MapGroup define endpoint execution at the end of the pipeline.

Routing, Endpoint Metadata, and Authorization

Routing selects an endpoint and attaches endpoint metadata to the current HttpContext. Middleware that needs endpoint metadata must run after routing.

Examples of endpoint metadata:

  • [Authorize]
  • [AllowAnonymous]
  • CORS metadata
  • Rate limiting metadata
  • Antiforgery metadata
  • Custom attributes used by custom middleware

Example custom middleware reading endpoint metadata:

Code
app.UseRouting();

app.Use(async (context, next) =>
{
    var endpoint = context.GetEndpoint();
    var requiresAudit = endpoint?.Metadata.GetMetadata<RequiresAuditAttribute>() is not null;

    if (requiresAudit)
    {
        Console.WriteLine($"Auditing request to {context.Request.Path}");
    }

    await next(context);
});

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

If this middleware runs before routing, context.GetEndpoint() will usually be null, so it cannot make decisions based on selected endpoint metadata.

Authentication Before Authorization

Authentication answers: "Who is the caller?"

Authorization answers: "Is the caller allowed to do this?"

Because authorization depends on knowing the user identity and claims, authentication should run before authorization:

Code
app.UseAuthentication();
app.UseAuthorization();

Incorrect order:

Code
app.UseAuthorization();
app.UseAuthentication();

With the incorrect order, authorization may evaluate an unauthenticated or incomplete HttpContext.User, causing protected endpoints to reject requests unexpectedly or behave incorrectly.

CORS Ordering

CORS controls whether browser-based cross-origin requests are allowed. CORS must be placed where it can apply the correct headers before the response is sent.

Common placement:

Code
app.UseRouting();

app.UseCors("FrontendPolicy");

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

A common mistake is placing CORS too late, after endpoint execution or after middleware that already produced a response. In that case, preflight requests may fail or cross-origin responses may miss required CORS headers.

Exception Handling Middleware

Exception handling middleware centralizes error handling for unhandled exceptions in later middleware and endpoints.

Production-style example:

Code
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/error");
    app.UseHsts();
}

Minimal API error endpoint:

Code
app.Map("/error", () => Results.Problem(
    title: "An unexpected error occurred.",
    statusCode: StatusCodes.Status500InternalServerError));

Important habits:

  • Do not expose stack traces or sensitive error details in production responses.
  • Log exceptions with enough context to troubleshoot.
  • Place exception handling early enough to catch downstream errors.
  • Remember that exception handling middleware does not catch errors from middleware registered before it.
  • Be careful if the error handler re-executes the pipeline, because middleware may need to be reentrant.

Custom Middleware Class

For reusable middleware, prefer a class instead of a large inline lambda.

Code
public sealed class CorrelationIdMiddleware
{
    private const string HeaderName = "X-Correlation-ID";
    private readonly RequestDelegate _next;
    private readonly ILogger<CorrelationIdMiddleware> _logger;

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

    public async Task InvokeAsync(HttpContext context)
    {
        var correlationId = context.Request.Headers.TryGetValue(HeaderName, out var value)
            ? value.ToString()
            : Guid.NewGuid().ToString("N");

        context.Items[HeaderName] = correlationId;
        context.Response.Headers.TryAdd(HeaderName, correlationId);

        using (_logger.BeginScope(new Dictionary<string, object>
        {
            [HeaderName] = correlationId
        }))
        {
            await _next(context);
        }
    }
}

Register it in the pipeline:

Code
app.UseMiddleware<CorrelationIdMiddleware>();

This middleware should usually be placed early so the correlation ID is available to downstream logging and services.

Middleware vs MVC Filters vs MediatR Behaviors

Middleware is not the only way to implement cross-cutting behavior.

MechanismScopeGood forNot ideal for
MiddlewareHTTP request/response pipelineLogging, exception handling, CORS, auth, rate limiting, headersUse-case-specific business rules
MVC filtersMVC/Razor action pipelineModel/action/result behavior, controller-specific concernsNon-MVC endpoints unless equivalent endpoint filters are used
Endpoint filtersMinimal API endpoint pipelineMinimal API validation, endpoint-level behaviorGlobal HTTP behavior that should apply before routing
MediatR pipeline behaviorsApplication request/handler pipelineValidation, transactions, application logging, use-case policiesHTTP-specific behavior like CORS or response headers
Domain servicesDomain/business layerBusiness rules and domain decisionsInfrastructure concerns like HTTP headers

Interviewers often expect developers to avoid forcing all cross-cutting logic into middleware. Good design places behavior at the correct layer.

Response Headers and HasStarted

Middleware often adds response headers, but headers must be set before the response starts.

Safe pattern:

Code
app.Use(async (context, next) =>
{
    context.Response.Headers.TryAdd("X-App-Version", "1.0");

    await next(context);
});

When a header depends on the final response, use OnStarting:

Code
app.Use(async (context, next) =>
{
    var startedAt = Stopwatch.GetTimestamp();

    context.Response.OnStarting(() =>
    {
        var elapsed = Stopwatch.GetElapsedTime(startedAt);
        context.Response.Headers.TryAdd("X-Elapsed-Milliseconds", elapsed.TotalMilliseconds.ToString("F0"));
        return Task.CompletedTask;
    });

    await next(context);
});

Avoid changing status code or headers after the response body has already started. Check context.Response.HasStarted when handling late errors or cleanup scenarios.

Request Body Reading and Reentrancy

Middleware can inspect the request body, but reading the body incorrectly can break downstream model binding or endpoint processing because the body stream may be consumed.

If middleware must read the body, enable buffering and reset the position:

Code
app.Use(async (context, next) =>
{
    context.Request.EnableBuffering();

    using var reader = new StreamReader(
        context.Request.Body,
        encoding: Encoding.UTF8,
        detectEncodingFromByteOrderMarks: false,
        bufferSize: 1024,
        leaveOpen: true);

    var body = await reader.ReadToEndAsync();
    context.Request.Body.Position = 0;

    await next(context);
});

Use this carefully. Reading large request bodies in middleware can hurt performance, increase memory usage, and create security risks if sensitive data is logged.

Dependency Injection in Middleware

Middleware classes can receive singleton-safe dependencies in the constructor and scoped dependencies in the InvokeAsync method.

Code
public sealed class TenantMiddleware
{
    private readonly RequestDelegate _next;

    public TenantMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context, ITenantResolver tenantResolver)
    {
        var tenant = await tenantResolver.ResolveAsync(context.RequestAborted);
        context.Items["Tenant"] = tenant;

        await _next(context);
    }
}

This matters because middleware instances are often constructed once and reused. Capturing scoped services in the constructor can cause lifetime problems unless the middleware is created using patterns that support per-request activation.

Performance Considerations

Middleware runs for many or all requests, so small inefficiencies can become expensive.

Best practices:

  • Keep middleware focused and lightweight.
  • Avoid unnecessary allocations on every request.
  • Avoid reading the request body unless required.
  • Short-circuit early for cheap endpoints such as health checks or static files when appropriate.
  • Place expensive middleware only where it is needed.
  • Use structured logging and avoid logging sensitive or high-volume payloads.
  • Prefer built-in middleware for common concerns when possible.

Example of branch-specific middleware:

Code
app.Map("/admin", adminApp =>
{
    adminApp.UseMiddleware<AdminAuditMiddleware>();
    adminApp.Run(async context =>
    {
        await context.Response.WriteAsync("Admin area");
    });
});

This avoids running admin-specific behavior for every public request.

Common Mistakes

Common mistakes include:

  • Registering UseAuthorization before UseAuthentication.
  • Placing CORS after endpoint execution.
  • Adding middleware after a terminal Run and expecting it to execute.
  • Putting exception handling too late to catch important failures.
  • Writing to the response body before calling next, then expecting downstream middleware to control the response.
  • Modifying headers after the response has started.
  • Reading the request body without resetting the stream.
  • Performing business logic in middleware when it belongs in the application or domain layer.
  • Creating middleware that is not reentrant when exception handling re-executes the pipeline.
  • Overusing global middleware when endpoint filters, MVC filters, or MediatR behaviors would be more precise.

Production-Oriented Ordering Example

A more production-oriented pipeline could look like this:

Code
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/error");
    app.UseHsts();
}

app.UseMiddleware<CorrelationIdMiddleware>();
app.UseMiddleware<RequestLoggingMiddleware>();

app.UseHttpsRedirection();
app.UseRequestLocalization();
app.UseStaticFiles();

app.UseRouting();

app.UseCors("FrontendPolicy");
app.UseAuthentication();
app.UseAuthorization();
app.UseRateLimiter();

app.MapHealthChecks("/health").AllowAnonymous();
app.MapControllers();

app.Run();

This order is not universal, but it demonstrates the reasoning:

  • Errors, correlation IDs, and logging happen early.
  • Security and localization happen before endpoint handling.
  • Routing happens before endpoint-aware middleware.
  • CORS, authentication, authorization, and rate limiting run before protected endpoints execute.
  • Endpoints are mapped at the end.

Interview Practice

PreviousDI, DI Container BasicsNext UpStructured Logging and Correlation