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:
Users were hitting endpoints that required authentication, but some UI routes did not check authentication before sending requests.
Slow database calls were not being tracked, which made performance debugging difficult.
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
| Type | Purpose |
|---|
| Built-in middleware | Provided by Microsoft (Routing, Authentication, Static Files) |
| Third-party middleware | Provided by libraries like Serilog, IdentityServer |
| Custom middleware | Created by developers to handle business-specific use cases |
Common Real-World Use Cases
Global error handling and exception logging
Authentication and permissions checks
Request throttling and rate limiting
API versioning logic
Enforcing HTTPS and security headers
Processing localization or culture settings
Request/response body transformation
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
Register middleware in the correct order.
Avoid heavy synchronous operations inside middleware.
Log meaningful context: request path, headers, user details, correlation IDs.
Create reusable middleware for shared behaviors.
Handle exceptions globally in a centralized middleware.
Use feature flags for optional middleware.
Common Mistakes to Avoid
| Mistake | Issue Caused |
|---|
| Wrong ordering | Authentication before routing breaks security |
| Writing directly to response prematurely | Later middleware or MVC cannot modify response |
| Performing database operations unnecessarily | Slows 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.