ASP.NET Core  

Understanding Middleware in ASP.NET Core: Internal Execution Flow, Real-World Use Cases, and Custom Middleware Implementation

Introduction

Every HTTP request that enters an ASP.NET Core application passes through a sequence of components called middleware. Each middleware can inspect, modify, validate, route, reject, or log the request before it reaches the controller or API endpoint. In many cases, middleware also handles the response on the way back.

In enterprise applications, middleware is essential for cross-cutting concerns such as logging, caching, authentication, authorization, routing, global exception handling, performance measurement, enforcing request limits, and request transformation.

This article explains how middleware works behind the scenes, how the ASP.NET Core request pipeline operates, and how we can design and build custom middleware for real-world production use cases.

This guide is written with a practical case-study mindset for senior developers.

Real-World Context and Problem Statement

A large e-commerce platform noticed three problems after launching a new promotional campaign:

  1. Users were hitting endpoints that required authentication, but some UI routes did not check authentication before sending requests.

  2. Slow database calls were not being tracked, which made performance debugging difficult.

  3. Security teams needed request metadata logging because some traffic appeared malicious.

Instead of wrapping every controller with repeated logic, the team solved all three issues using middleware:

  • A global request logger middleware,

  • A performance monitoring middleware,

  • A permission-verification middleware for protected endpoints.

This approach reduced duplicated code, improved maintainability, and provided centralized request handling.

How Middleware Works Internally

ASP.NET Core follows a pipeline-based architecture, meaning:

  • Middleware is executed in the order it is registered.

  • Each middleware can either continue the flow or stop the request.

  • The response travels backward through the pipeline (reverse order).

In simple terms

Request --> [Middleware1] --> [Middleware2] --> [Middleware3] --> Controller
Response <-- [Middleware1] <-- [Middleware2] <-- [Middleware3]

If any middleware decides to stop the pipeline, later middlewares are skipped.

Types of Middleware

TypePurpose
Built-in middlewareProvided by Microsoft (Routing, Authentication, Static Files)
Third-party middlewareProvided by libraries like Serilog, IdentityServer
Custom middlewareCreated by developers to handle business-specific use cases

Common Real-World Use Cases

  1. Global error handling and exception logging

  2. Authentication and permissions checks

  3. Request throttling and rate limiting

  4. API versioning logic

  5. Enforcing HTTPS and security headers

  6. Processing localization or culture settings

  7. Request/response body transformation

  8. Auditing, analytics, and telemetry

Minimal Pipeline Example

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Use(async (context, next) =>
{
    Console.WriteLine("Before request");
    await next.Invoke();
    Console.WriteLine("After response");
});

app.MapGet("/", () => "Hello World");

app.Run();

Output when calling /:

Before requestAfter response

This demonstrates how middleware wraps execution.

Workflow Diagram

Client Request
     |
     V
Static Files Middleware
     |
     V
Authentication Middleware
     |
     V
Authorization Middleware
     |
     V
Routing Middleware
     |
     V
Controller / Endpoint
     |
     V
Response Processing (reverse order)
     |
     V
Return Response to Client

Flowchart of Middleware Execution

 ┌──────────────────────────┐
 │ Start HTTP Request       │
 └───────────────┬─────────┘
                 │
                 ▼
     ┌──────────────────────────┐
     │ Execute Next Middleware  │
     └───────────────┬─────────┘
                     │
                     ▼
     ┌───────────────────────────┐
     │ Does it block or return? │
     └───────┬────────────┬─────┘
             │             │
            Yes           No
             │             │
             ▼             ▼
 ┌──────────────────┐     ┌─────────────────────┐
 │ Return Response  │     │ Execute Controller  │
 └──────────────────┘     └─────────────────────┘
                             │
                             ▼
              ┌────────────────────────────────┐
              │ Response flows back through    │
              │ middleware in reverse order    │
              └────────────────────────────────┘

Creating Custom Middleware (Example: Request Logging)

Step 1: Create the Middleware Class

public class RequestLoggingMiddleware
{
    private readonly RequestDelegate _next;

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

    public async Task InvokeAsync(HttpContext context)
    {
        var method = context.Request.Method;
        var path = context.Request.Path;
        Console.WriteLine($"Incoming request: {method} {path}");

        await _next(context);

        Console.WriteLine($"Response Status: {context.Response.StatusCode}");
    }
}

Step 2: Register Middleware in Program.cs

app.UseMiddleware<RequestLoggingMiddleware>();

Advanced Example: Performance Tracking Middleware

public class PerformanceMiddleware
{
    private readonly RequestDelegate _next;

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

    public async Task InvokeAsync(HttpContext context)
    {
        var stopwatch = Stopwatch.StartNew();

        await _next(context);

        stopwatch.Stop();

        if (stopwatch.ElapsedMilliseconds > 500)
        {
            Console.WriteLine($"Slow request detected: {context.Request.Path} took {stopwatch.ElapsedMilliseconds}ms");
        }
    }
}

Best Practices

  1. Register middleware in the correct order.

  2. Avoid heavy synchronous operations inside middleware.

  3. Log meaningful context: request path, headers, user details, correlation IDs.

  4. Create reusable middleware for shared behaviors.

  5. Handle exceptions globally in a centralized middleware.

  6. Use feature flags for optional middleware.

Common Mistakes to Avoid

MistakeIssue Caused
Wrong orderingAuthentication before routing breaks security
Writing directly to response prematurelyLater middleware or MVC cannot modify response
Performing database operations unnecessarilySlows down overall pipeline
Not calling await next()Blocks entire request execution

Testing Middleware

Middleware can be unit tested using WebApplicationFactory<T>.

[Test]
public async Task Middleware_ShouldLogRequest()
{
    var application = new WebApplicationFactory<Program>();
    var client = application.CreateClient();

    var response = await client.GetAsync("/test");

    Assert.IsTrue(response.IsSuccessStatusCode);
}

Final Recommendations

  • Use built-in middleware wherever possible to avoid redundancy.

  • Keep middleware focused and avoid mixing responsibilities.

  • Maintain a global error handling middleware to keep responses consistent.

  • Pair middleware with logging and metrics platforms such as Application Insights, Serilog, and OpenTelemetry.

Conclusion

Middleware is the foundation of the ASP.NET Core request pipeline. It provides a clean and consistent structure to apply cross-cutting behaviors without modifying individual controllers or services.

By designing middleware thoughtfully and organizing it in a predictable order, engineers can create applications that are secure, maintainable, observable, and scalable.