
Whenever a request hits an ASP.NET Core application, it doesn’t directly go to your controller or endpoint. Before that happens, the request passes through multiple components that can inspect it, modify it, log it, validate it, or even stop it completely.
Those components are called middleware.
If you’ve worked with ASP.NET Core APIs before, you’ve already used middleware even if you didn’t realize it.
Things like:
authentication
authorization
exception handling
CORS
request logging
static files
rate limiting
are all implemented using middleware internally.
Understanding middleware is important because once you understand how the request pipeline actually works, ASP.NET Core starts making much more sense.
Instead of feeling like “framework magic”, you start seeing how requests are flowing through the application step by step.
The Request Pipeline
The easiest way to think about middleware is as a chain.
A request enters the application and moves through middleware one by one until it finally reaches the controller.
Then the response travels back through the same middleware in reverse order.
Request
↓
Middleware
↓
Middleware
↓
Controller
↓
Response
That reverse flow is the important part that many developers initially miss.
Middleware doesn’t just run once.
It runs:
before the next middleware
and again after the response comes back
That’s what makes middleware powerful.
You can:
inspect requests
inspect responses
measure execution time
handle exceptions
add headers
terminate requests
apply cross-cutting concerns globally
A typical middleware pipeline in ASP.NET Core looks something like this:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
The order here matters a lot.
Middleware executes in the same order it gets registered.
If you accidentally place authentication after authorization, things break.
If exception middleware is added too late, exceptions won’t be caught properly.
Middleware order is one of the most important parts of the ASP.NET Core pipeline.
Understanding How Middleware Actually Executes
Let’s take a simple request to:
GET /api/weather
Internally, the flow looks something like this:
Middleware A Start
↓
Middleware B Start
↓
Controller
↓
Middleware B End
↓
Middleware A End
Notice how the request enters from the top and then comes back upward again after the controller finishes.
That’s because every middleware decides when to pass execution to the next middleware and when execution returns back.
This is why middleware is so useful for logging and tracing.
You can see the entire request lifecycle without touching controller code.
Creating Inline Middleware with app.Use()
The simplest way to create middleware is directly inside Program.cs using app.Use().
app.Use(async (context, next) =>
{
Console.WriteLine(
$"Request Started: {context.Request.Path}");
await next();
Console.WriteLine(
$"Response Status: {context.Response.StatusCode}");
});
This middleware runs for every request.
The important thing here is:
await next();
That line passes execution to the next middleware in the pipeline.
Without it, the request stops there.
A lot of middleware behavior becomes easy to understand once you realize that middleware is basically:
“do something before next(), then optionally do something after next()”
Code before await next() runs before the controller.
Code after await next() runs after the response comes back.
This pattern is commonly used for:
logging
tracing
timing
diagnostics
response modification
Inline middleware is great for smaller logic.
But once the middleware becomes larger, using a dedicated class is usually cleaner.
Creating Custom Middleware Classes
For reusable middleware, ASP.NET Core typically uses middleware classes.
Here’s a simple request logging middleware.
public sealed 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)
{
var stopwatch = Stopwatch.StartNew();
_logger.LogInformation(
"Request Started: {Method} {Path}",
context.Request.Method,
context.Request.Path);
await _next(context);
stopwatch.Stop();
_logger.LogInformation(
"Request Finished: {StatusCode} | {ElapsedMs}ms",
context.Response.StatusCode,
stopwatch.ElapsedMilliseconds);
}
}
A conventional middleware class usually contains:
a constructor
RequestDelegate
an InvokeAsync() method
The important line again is:
await _next(context);
That’s what continues the pipeline.
Without it:
controller never executes
next middleware never executes
request ends immediately
And that behavior is actually useful in some scenarios.
Middleware gets registered like this:
app.UseMiddleware<RequestLoggingMiddleware>();
What Exactly is RequestDelegate?
You’ll see RequestDelegate everywhere in middleware.
Internally it’s basically this:
public delegate Task RequestDelegate(HttpContext context);
It represents:
“the next middleware in the pipeline”
So when you call:
await _next(context);
you’re telling ASP.NET Core:
“continue processing the request”
If you don’t call it, the request pipeline stops there.
This is how terminating middleware works.
Terminating Middleware
Some middleware intentionally stops the pipeline and returns a response directly.
A maintenance middleware is a good example.
public sealed class MaintenanceMiddleware
{
private readonly RequestDelegate _next;
public MaintenanceMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
context.Response.StatusCode =
StatusCodes.Status503ServiceUnavailable;
await context.Response.WriteAsync(
"Service is temporarily unavailable.");
}
}
Notice something important here.
There’s no:
await _next(context);
So the request never continues.
The controller is never reached.
This middleware directly returns a response and ends the request pipeline.
It can be mapped only to specific routes.
app.Map("/maintenance", maintenanceApp =>
{
maintenanceApp.UseMiddleware<MaintenanceMiddleware>();
});
So only /maintenance requests get terminated.

Join the conversation! Your thoughts help the community grow.