
What Is Middleware?
When an HTTP request arrives at an ASP.NET Core application, it does not simply jump straight into a controller, Razor Page or endpoint. Instead, it passes through a pipeline made up of individual pieces of software called middleware.
Each middleware component has an opportunity to examine the request, perform some processing and then decide what happens next.
A middleware component can pass the request to the next component in the pipeline, modify the request before passing it onwards, modify the response as it comes back, or stop processing altogether and return a response directly to the client.
This makes middleware extremely powerful because it provides a central place for functionality that applies to multiple requests.
Common examples include exception handling, authentication, authorisation, HTTPS redirection, static files, logging, CORS and response compression.
The ASP.NET Core Request Pipeline
The middleware pipeline is configured in Program.cs.
A simple ASP.NET Core application might contain code such as:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseHttpsRedirection();
app.Run(async context =>
{
await context.Response.WriteAsync("Hello from ASP.NET Core");
});
app.Run();
The important thing to understand here is that the middleware is executed in the order in which it is added.
The request first reaches UseHttpsRedirection. If the request needs to be redirected to HTTPS, that middleware can handle the request without allowing it to continue.
If the request continues, it eventually reaches the Run delegate, which produces the response.
The pipeline is therefore not just a collection of unrelated services. It is an ordered chain of request processing components.
The Use Method
The Use method is commonly used when creating middleware that performs some work and then allows the request to continue.
For example:
app.Use(async (context, next) =>
{
Console.WriteLine("Before the next middleware");
await next();
Console.WriteLine("After the next middleware");
});
There are two important parts to this example.
The first Console.WriteLine runs before the request is passed further down the pipeline.
The call to next() transfers control to the next middleware.
Once the next middleware has finished processing, execution returns to this middleware and the second Console.WriteLine is executed.
This means middleware effectively surrounds the middleware that comes after it.
That behaviour is particularly useful when working with logging, timing, response manipulation and other functionality that needs to perform work both before and after the rest of the request pipeline.
Understanding Run
The Run method is different because it is normally used for terminal middleware.
A terminal middleware does not pass the request on to another middleware component.
For example:
app.Run(async context =>
{
await context.Response.WriteAsync("This is the end of the pipeline");
});
Once the request reaches this point, processing stops.
Any middleware registered after this Run delegate will not be executed.
This is an important distinction between Use and Run. Use normally continues the pipeline by calling next, whereas Run terminates the pipeline.
If a piece of middleware is deliberately intended to finish the request, Run is generally the clearer choice. Microsoft also recommends using Run when the middleware never calls the next delegate.
Middleware Runs in Both Directions
One of the easiest ways to understand middleware is to imagine the request travelling down a chain and the response travelling back up that same chain.
Consider this example:
app.Use(async (context, next) =>
{
Console.WriteLine("Middleware 1 - Before");
await next();
Console.WriteLine("Middleware 1 - After");
});
app.Use(async (context, next) =>
{
Console.WriteLine("Middleware 2 - Before");
await next();
Console.WriteLine("Middleware 2 - After");
});
app.Run(async context =>
{
Console.WriteLine("Endpoint");
await context.Response.WriteAsync("Hello");
});
The output will effectively be:
Middleware 1 - Before Middleware 2 - Before Endpoint Middleware 2 - After Middleware 1 - After
This is sometimes described as a Russian-doll or onion-style pipeline.
The request travels into the middleware layers, reaches the endpoint and then unwinds back through those layers.
This is why middleware can perform useful work after calling next(). For example, a timing middleware could record the start time before calling next() and calculate the elapsed time afterwards.
Why Middleware Order Matters
The order of middleware is one of the most important concepts to understand.
For example, authentication and authorisation are related, but they perform different jobs and normally need to appear in the appropriate order.
A typical application might contain:
app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers();
Authentication establishes who the user is.
Authorisation determines whether that user is allowed to access a particular resource.
Putting middleware in the wrong order can result in unexpected behaviour, security problems or functionality that simply does not work.
Microsoft's documentation specifically highlights that middleware ordering is important for security, performance and functionality.
Exception Handling Middleware
Exception handling is one of the best examples of why middleware order matters.
In a production application, exception handling middleware is generally placed very early in the pipeline.
For example:
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
By placing the exception handler early, exceptions generated by middleware further down the pipeline can be intercepted and handled appropriately.
This gives you a central location for handling unexpected exceptions rather than having to surround individual pieces of application code with try/catch blocks.
It is important to remember that exception handling middleware is not a replacement for sensible error handling within your application. It provides a final layer for handling exceptions that escape the normal application flow.
Static Files and Short-Circuiting
Middleware does not always have to call the next component.
Static file middleware is a good example.
If a browser requests:
/css/site.css
and the file exists, the static file middleware can return that file directly.
There is no reason to send the request through controllers or other application components.
The pipeline has effectively been short-circuited.
Short-circuiting can improve performance because unnecessary processing is avoided.
However, it is important to understand the security implications. Files served by the standard static file middleware are publicly available, so sensitive files should not simply be placed somewhere that static file middleware can serve them.
Creating Custom Middleware
One of the strengths of ASP.NET Core is that you can create your own middleware.
Suppose you wanted to log how long every request takes.
A custom middleware class could look like this:
public class RequestTimingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestTimingMiddleware> _logger;
public RequestTimingMiddleware(
RequestDelegate next,
ILogger<RequestTimingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
await _next(context);
stopwatch.Stop();
_logger.LogInformation(
"Request {Path} took {ElapsedMilliseconds} ms",
context.Request.Path,
stopwatch.ElapsedMilliseconds);
}
}
The RequestDelegate represents the next component in the pipeline.
The InvokeAsync method is called for each request that reaches this middleware.
The stopwatch is started before the request continues and stopped afterwards.
This provides a simple way of measuring the time taken by the downstream pipeline.
Registering Custom Middleware
The custom middleware can then be added to Program.cs.
For example:
app.UseMiddleware<RequestTimingMiddleware>();
The position of this line determines where the middleware sits in the request pipeline.
If it is placed near the beginning of the pipeline, it can measure a large part of the application's processing time.
If it is placed further down the pipeline, it will only measure the processing that occurs after it.
This is another example of why understanding middleware ordering is so important.
Microsoft's current ASP.NET Core documentation also demonstrates registering custom middleware using UseMiddleware<T>.
Middleware and Dependency Injection
Custom middleware can also work with ASP.NET Core's dependency injection system.
For example, the ILogger<RequestTimingMiddleware> used in the previous example is automatically supplied by the framework.
This allows middleware to use services registered in the application's dependency injection container rather than manually creating those services.
For more complex middleware that needs services with particular lifetimes, ASP.NET Core also provides factory-based middleware activation.
This is worth considering when middleware depends on services that need to be resolved on a per-request basis.
Middleware Compared with MVC Filters
Middleware and MVC filters can sometimes appear to solve the same problems, but they operate at different levels.
Middleware sits within the application's HTTP request pipeline and can therefore operate across the entire application.
MVC filters operate within the MVC request processing pipeline and are more closely associated with controllers and actions.
For example, if you wanted to log every HTTP request reaching your application, middleware would be a natural choice.
If you wanted functionality specifically associated with MVC controller actions, an MVC filter might be more appropriate.
Choosing the correct level of abstraction helps keep your application architecture clean.
Branching the Middleware Pipeline
ASP.NET Core also allows you to branch the pipeline.
The Map method can be used when you want a particular path to use a different branch.
For example:
app.Map("/admin", adminApp =>
{
adminApp.Run(async context =>
{
await context.Response.WriteAsync("Admin area");
});
});
A request to /admin can therefore be handled by this branch rather than following the normal pipeline.
There is also MapWhen, which allows a branch to be selected using a condition.
This can be useful when the decision is based on something more complex than the URL path.
For example, a branch could be selected based on a header, query-string value or another property of HttpContext.
Common Middleware in ASP.NET Core
Most ASP.NET Core applications use several middleware components, even if you never write a custom one yourself.
HTTPS redirection ensures HTTP requests are redirected to HTTPS.
Static file middleware serves files such as CSS, JavaScript, images and other static content.
Routing determines which endpoint should handle a request.
Authentication identifies the current user.
Authorisation determines whether that user has permission to access a resource.
Exception handling provides centralised handling of unhandled exceptions.
CORS controls which external origins are permitted to make cross-origin requests.
Response compression can reduce the amount of data sent to clients.
Rate limiting can control how frequently clients are allowed to access resources.
These components can be combined to create the request pipeline appropriate for your application.
A Practical Middleware Pipeline
A typical modern ASP.NET Core application might have a pipeline resembling:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
The exact pipeline will vary depending on whether you are building an MVC application, Web API, Razor Pages application, Blazor application or another type of ASP.NET Core application.
There is no single middleware ordering that is correct for every project.
The important thing is to understand what each component does and what it expects to happen before and after it.
Middleware Is About the Pipeline
Middleware can initially seem complicated because there are so many different components available.
However, the underlying idea is relatively simple.
A request enters the application and passes through a sequence of middleware components. Each component can inspect or modify the request, perform some work, pass control to the next component and then perform additional work when control returns.
Eventually, the request reaches an endpoint which produces a response.
The response then travels back through the middleware that called the next component.
Once you understand that flow, many ASP.NET Core features become much easier to understand.
Final Thoughts
Middleware is one of the fundamental concepts behind ASP.NET Core.
It provides a clean and flexible way of handling functionality that sits around the processing of HTTP requests and responses. Rather than duplicating logging, authentication, exception handling or other cross-cutting functionality throughout an application, middleware allows these concerns to be placed into the request pipeline.
The most important lesson is to pay attention to order. Middleware executes in the order in which it is registered for the incoming request and unwinds in the reverse order for the response. Getting that order right is often the difference between an ASP.NET Core application that behaves exactly as expected and one that produces confusing problems.
Once middleware becomes familiar, Program.cs stops looking like a collection of mysterious Use and Map calls and starts to look like what it really is: the definition of how your application processes an HTTP request.
Become a member
Get the latest news right in your inbox. It's free and you can unsubscribe at any time. We hate spam as much as we do, so we never spam!
Read next
Logging Strategies That Scale
Logging is easy when an application is small, but as systems grow, so does the amount of information they generate. Without a sensible strategy, logs can quickly become noisy, expensive and difficult to search. In this article, we look at practical logging strategies for modern .NET applications, covering structured logging, log levels, performance, centralisation and how to make logs genuinely useful when troubleshooting production systems.
Background Services in ASP.NET Core
Minimal APIs vs Controllers: A Real Comparison
