Comprehending.NET MiddlewareA request does not proceed straight to your controller or endpoint when it reaches an ASP.NET Core application. The request first goes through a number of components that have the ability to examine, alter, log, validate, or even halt it entirely.
We refer to those parts as middleware.
Even if you weren't aware of it, you have already utilized middleware if you have previously worked with ASP.NET Core APIs.
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.
Other requests continue normally.
The IMiddleware Pattern
ASP.NET Core also provides another middleware pattern using IMiddleware.
public sealed class HeaderValidationMiddleware : IMiddleware
{
public async Task InvokeAsync(
HttpContext context,
RequestDelegate next)
{
if (!context.Request.Headers.TryGetValue(
"x-api-key",
out var apiKey))
{
context.Response.StatusCode =
StatusCodes.Status401Unauthorized;
await context.Response.WriteAsJsonAsync(
new ProblemDetails
{
Title = "Unauthorized",
Status = 401,
Detail = "x-api-key header is required."
});
return;
}
await next(context);
}
}
This approach is slightly different from conventional middleware.
With IMiddleware:
- middleware gets activated from DI
- RequestDelegate comes directly in InvokeAsync
- middleware feels more service-oriented
Registration happens through dependency injection.
builder.Services.AddTransient<HeaderValidationMiddleware>();
app.UseMiddleware<HeaderValidationMiddleware>();
You won’t always need IMiddleware, but it’s useful when middleware depends heavily on dependency injection or scoped services.
Dependency Injection Inside Middleware
Middleware supports dependency injection just like controllers.
Constructor injection works normally.
public RequestLoggingMiddleware(
RequestDelegate next,
ILogger<RequestLoggingMiddleware> logger)
{
}
you can also inject services directly into InvokeAsync().
public async Task InvokeAsync(
HttpContext context,
RequestIdService requestIdService)
{
}
This is especially important for scoped services.
Scoped Services in Middleware
This is one of the most common middleware mistakes developers run into.
Scoped services should generally be injected into InvokeAsync() instead of the middleware constructor.
Example scoped service registration:
builder.Services.AddScoped<RequestIdService>();
Scoped service:
public sealed class RequestIdService
{
public Guid RequestId { get; } = Guid.NewGuid();
}
Using it inside middleware:
public async Task InvokeAsync(
HttpContext context,
RequestIdService requestIdService)
{
context.Response.Headers["X-Request-Id"] =
requestIdService.RequestId.ToString();
await _next(context);
}
Each request gets its own scoped instance.
That means every request receives a different request ID.
Middleware itself behaves more like a singleton because it’s created once for the pipeline.
That’s why scoped services inside constructors can create lifetime problems.
This is one of those things that feels confusing initially until you actually debug request lifetimes.
Global Exception Handling Middleware
Exception handling is one of the most common real-world middleware use cases.
A global exception middleware usually wraps the request pipeline in a try-catch block.
public sealed 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 exception)
{
_logger.LogError(
exception,
"Unhandled exception");
context.Response.StatusCode =
StatusCodes.Status500InternalServerError;
await context.Response.WriteAsJsonAsync(
new ProblemDetails
{
Title = "An unexpected error occurred.",
Status = 500,
Detail = exception.Message
});
}
}
}
This middleware should usually be registered early in the pipeline.
app.UseMiddleware<GlobalExceptionMiddleware>();
That way it wraps everything below it.
If a controller or downstream middleware throws an exception, this middleware catches it and returns a clean standardized response.
Without centralized exception middleware, exception handling becomes repetitive very quickly.
Conditional Middleware with UseWhen()
Sometimes middleware should run only for specific requests.
ASP.NET Core provides UseWhen() for this.
app.UseWhen(context => context.Request.Path.StartsWithSegments("/api/secure"),
secureApp =>
{
secureApp.UseMiddleware<HeaderValidationMiddleware>();
});
In this example:
- middleware only runs for /api/secure
- other requests skip it entirely
This is useful for:
- API key validation
- feature-specific middleware
- route-specific authentication
- admin-only middleware
- specialized logging
Use vs Run vs Map
Three middleware registration methods are important to understand.
app.Use()
Adds middleware into the pipeline.
app.Use(async (context, next) =>
{
await next();
});
Pipeline continues only if next() is called.
app.Run()
Terminates the pipeline.
app.Run(async context =>
{
await context.Response.WriteAsync(
"Pipeline ended.");
});
No middleware executes after this.
app.Map()
Creates a branch pipeline.
app.Map("/maintenance", maintenanceApp =>
{
maintenanceApp.UseMiddleware<MaintenanceMiddleware>();
});
Only matching routes enter this branch.
This is commonly used for:
- admin routes
- health checks
- feature-specific pipelines
- versioned APIs
Middleware Extension Methods
Most middleware in ASP.NET Core gets registered using extension methods.
Instead of writing:
app.UseMiddleware<RequestLoggingMiddleware>();
you can create cleaner registration methods.
public static class ApplicationBuilderExtensions
{
public static IApplicationBuilder UseRequestLogging(
this IApplicationBuilder app)
{
return app.UseMiddleware<RequestLoggingMiddleware>();
}
}
Then registration becomes cleaner.
app.UseRequestLogging();
This is the same pattern used throughout ASP.NET Core itself.
Understanding a Real Request Flow
Once you understand middleware individually, the interesting part is seeing how everything works together.
Here’s the flow for a typical request:
Request arrives
↓
Global Exception Middleware
↓
Logging Middleware
↓
Authentication Middleware
↓
Controller
↓
Logging Response
↓
Response sent
Now imagine a request where authentication fails.
Request arrives
↓
Global Exception Middleware
↓
Logging Middleware
↓
Authentication Middleware
- validation failed
- returns 401
- pipeline stops
The controller never executes because middleware terminated the request.
This is middleware behavior in action.
Best Practices
A few middleware practices become important once applications grow.
Keep middleware focused.
Logging middleware should log. Validation middleware should validate. Exception middleware should handle exceptions.
Avoid mixing too many responsibilities into one middleware.
Middleware order also matters a lot.
Exception middleware should usually come early. Authentication should run before authorization.
Another important thing: middleware is best for cross-cutting concerns.
Heavy business logic usually belongs inside services or application layers, not middleware.
And finally: trace requests while learning middleware.
Once you start observing request flow through logs, middleware becomes much easier to understand.
Conclusion
Middleware is one of the core building blocks of ASP.NET Core. Once you understand how the request pipeline works, a lot of ASP.NET Core architecture suddenly becomes easier to reason about.
The important things to remember are:
- middleware executes in registration order
- responses travel back in reverse order
- next() continues the pipeline
- omitting next() terminates the request
- middleware fully supports dependency injection
- most ASP.NET Core features internally rely on middleware
The best way to truly understand middleware is to build some yourself, trace requests, and observe how execution flows through the pipeline. That’s usually the point where middleware stops feeling abstract and starts becoming practical. And if you want to explore the complete implementation, you can find the full source code on my GitHub repository.
European Best, cheap and reliable ASP.NET Core 10.0 hosting with instant activation. HostForLIFE.eu is #1 Recommended Windows and ASP.NET hosting in European Continent. With 99.99% Uptime Guaranteed of Relibility, Stability and Performace. HostForLIFE.eu security team is constantly monitoring the entire network for unusual behaviour. We deliver hosting solution including Shared hosting, Cloud hosting, Reseller hosting, Dedicated Servers, and IT as Service for companies of all size.
