Understanding Middleware in ASP.NET Core
Category: ASP.NET Core | Reading Time: ~7 minutes | Audience: .NET Developers
Introduction
If you’ve spent any time building web applications with ASP.NET Core, you’ve encountered middleware — even if you didn’t realise it. Every time a request hits your application, it travels through a carefully ordered pipeline of components before a response is returned. That pipeline is made up entirely of middleware.
Understanding how middleware works is one of the most important skills you can develop as an ASP.NET Core developer. It gives you fine-grained control over how requests and responses are processed, opens the door to powerful cross-cutting concerns like authentication, logging, and error handling, and helps you reason about your application’s behaviour with confidence.
In this post, we’ll break down what middleware is, how the request pipeline works, how to write your own custom middleware, and some practical patterns you’ll use in real-world Australian enterprise applications.
What Is Middleware?
In ASP.NET Core, middleware is software assembled into an application pipeline to handle HTTP requests and responses. Each piece of middleware:
- Receives an
HttpContextobject representing the incoming request and outgoing response - Can perform operations before passing control to the next component
- Can pass control to the next middleware in the pipeline (or short-circuit and return early)
- Can perform operations after the next component has processed the request
Think of it like a series of filters stacked on top of each other. The request passes down through each filter, reaches the endpoint (your controller or minimal API), and the response then bubbles back up through those same filters in reverse.
The Request Pipeline
The ASP.NET Core request pipeline is configured inside the Program.cs file (or Startup.cs in older projects). You register middleware using extension methods on WebApplication or IApplicationBuilder.
Here’s a simple example:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseExceptionHandler("/error");
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
The order matters enormously. Middleware is executed in the order it’s registered. In the example above, exception handling is registered first so it can catch errors thrown by any subsequent middleware. Authentication runs before authorisation — if you swap those, your application will behave incorrectly.
Types of Middleware Registration
ASP.NET Core gives you three ways to connect a middleware component into the pipeline:
1. Use — Pass to the Next Component
Use adds middleware that can call the next delegate, allowing processing both before and after the next component.
app.Use(async (context, next) =>
{
// Logic before the next middleware
Console.WriteLine($"Incoming: {context.Request.Path}");
await next.Invoke(); // Call the next middleware
// Logic after the next middleware
Console.WriteLine($"Outgoing: {context.Response.StatusCode}");
});
2. Run — Terminal Middleware
Run adds a terminal middleware delegate that does not call the next component. It ends the pipeline.
app.Run(async context =>
{
await context.Response.WriteAsync("Pipeline ends here.");
});
3. Map — Branching the Pipeline
Map branches the pipeline based on a URL path match.
app.Map("/health", healthApp =>
{
healthApp.Run(async context =>
{
await context.Response.WriteAsync("Healthy");
});
});
This is particularly useful for health check endpoints, API versioning, or routing to specialised sub-pipelines.
Writing Custom Middleware
While ASP.NET Core ships with many built-in middleware components, the real power comes from writing your own.
Inline Middleware (Quick and Simple)
For simple cases, use the Use pattern inline in Program.cs:
app.Use(async (context, next) =>
{
if (!context.Request.Headers.ContainsKey("X-Api-Key"))
{
context.Response.StatusCode = 401;
await context.Response.WriteAsync("Unauthorised: Missing API key.");
return;
}
await next.Invoke();
});
Class-Based Middleware (Recommended for Production)
For anything non-trivial, encapsulate your middleware logic in a dedicated class. This improves testability, separation of concerns, and reusability.
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
_logger.LogInformation("Handling request: {Method} {Path}",
context.Request.Method,
context.Request.Path);
await _next(context);
_logger.LogInformation("Response status: {StatusCode}",
context.Response.StatusCode);
}
}
Then register it with an extension method:
public static class RequestLoggingMiddlewareExtensions
{
public static IApplicationBuilder UseRequestLogging(this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestLoggingMiddleware>();
}
}
And wire it up cleanly:
app.UseRequestLogging();
Common Real-World Use Cases
Middleware is the right tool whenever you need logic that applies broadly across your application. Here are some patterns you’ll encounter regularly:
Global Exception Handling
public class GlobalExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<GlobalExceptionMiddleware> _logger;
public GlobalExceptionMiddleware(RequestDelegate next, ILogger<GlobalExceptionMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "An unhandled exception occurred.");
context.Response.StatusCode = 500;
await context.Response.WriteAsJsonAsync(new { error = "An unexpected error occurred." });
}
}
}
This is far preferable to scattering try/catch blocks throughout your controllers.
Correlation ID Propagation
In microservices and distributed systems — increasingly common in Australian enterprise environments — you need to trace a request across multiple services. A correlation ID middleware can attach a unique identifier to every request:
public class CorrelationIdMiddleware
{
private const string CorrelationIdHeader = "X-Correlation-ID";
private readonly RequestDelegate _next;
public CorrelationIdMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext context)
{
var correlationId = context.Request.Headers[CorrelationIdHeader].FirstOrDefault()
?? Guid.NewGuid().ToString();
context.Response.Headers[CorrelationIdHeader] = correlationId;
using (Serilog.Context.LogContext.PushProperty("CorrelationId", correlationId))
{
await _next(context);
}
}
}
Request Timing
Want to know how long each request is taking in production? A timing middleware makes it trivial:
app.Use(async (context, next) =>
{
var stopwatch = Stopwatch.StartNew();
await next.Invoke();
stopwatch.Stop();
context.Response.Headers["X-Response-Time-Ms"] = stopwatch.ElapsedMilliseconds.ToString();
});
Middleware vs. Filters vs. Action Filters
A common question is when to use middleware versus ASP.NET Core’s filter pipeline (action filters, exception filters, etc.).
The key distinction is scope and context:
| Middleware | Filters | |
|---|---|---|
| Level | Application-wide | MVC/Controller-specific |
| Access to MVC context | No | Yes |
| Execution order | Before MVC pipeline | Inside MVC pipeline |
| Best for | Cross-cutting infrastructure | MVC-specific behaviour |
Use middleware for concerns that apply regardless of MVC — logging, compression, CORS, authentication. Use filters for concerns tied to controller actions — model validation, response transformation, action-specific caching.
Built-In Middleware You Should Know
ASP.NET Core ships with a rich set of built-in middleware components:
UseExceptionHandler— Global error handling and friendly error pagesUseHsts— HTTP Strict Transport Security headersUseHttpsRedirection— Redirects HTTP to HTTPSUseStaticFiles— Serves static assets fromwwwrootUseCors— Configures Cross-Origin Resource SharingUseAuthentication— Identifies the current userUseAuthorization— Enforces access policiesUseResponseCompression— Compresses HTTP responsesUseResponseCaching— Server-side response cachingUseRateLimiter— Rate limiting (added in .NET 7)
Knowing these components and their correct order is foundational knowledge for any ASP.NET Core developer.
Performance Considerations
Middleware runs on every request, so poorly written middleware can have significant performance implications.
A few guidelines:
- Keep middleware focused and lightweight. Avoid blocking I/O without
async/await. - Use dependency injection for services but be aware of service lifetimes.
RequestDelegatemiddleware is instantiated once (singleton-like behaviour), so be careful with scoped dependencies — retrieve them fromcontext.RequestServicesinsideInvokeAsyncrather than injecting them via the constructor. - Short-circuit early when possible. If a request doesn’t need to proceed further, return immediately.
// Prefer this — resolve scoped services inside InvokeAsync
public async Task InvokeAsync(HttpContext context, IServiceProvider services)
{
var scopedService = services.GetRequiredService<IMyScopedService>();
// ...
}
Conclusion
Middleware is the backbone of how ASP.NET Core processes HTTP traffic. Whether you’re building REST APIs, web applications, or microservices, a solid understanding of the middleware pipeline will make you a more effective and confident .NET developer.
The key takeaways:
- Middleware processes requests and responses in a defined, ordered pipeline
- Order of registration matters — always think about which middleware must run first
- Write class-based middleware for anything you’ll reuse or need to test
- Use middleware for cross-cutting concerns (logging, auth, error handling, tracing)
- Know the built-in components and their correct order in the pipeline
Start simple, build incrementally, and use middleware to keep your application logic clean and your pipeline explicit. Your future self — and your teammates — will thank you.
Written for .NET professionals building modern applications on ASP.NET Core. For questions, feedback, or consulting engagements, get in touch with our team.
Tags: ASP.NET Core, Middleware, .NET, C#, Web Development, Backend
