Middleware
Middleware is software that’s assembled into an app pipeline to handle requests and responses. Each component: Chooses whether to pass the request to the next component in the pipeline; Can perform work before and after the next component in the pipeline.
An individual request delegate can be specified in-line as an anonymous method (called in-line middleware), or it can be defined in a reusable class. These reusable classes and in-line anonymous methods are middleware, also called middleware components. When a middleware short-circuits, it's called a terminal middleware because it prevents further middleware from processing the request.
Use Run and Map
Use: Add a middleware and allow it to proceed to the next one (chain invocation).
Run: Add a terminal middleware that ends the request without passing it to the next one.
Map: Route the middleware pipeline based on path conditions, acting as a request dispatcher.
Use
app.Use(async (context, next) =>
{
Console.WriteLine("Before");
await next(); // invoke next request delegate
Console.WriteLine("After");
});
It is suitable for constructing a "pre-processing + post-processing" structure (e.g., logging, performance timing). The following middleware will only be executed after the current one calls await next(). Middleware can be written inline or encapsulated as a separate class and registered using app.UseMiddleware<>().
Run
app.Run(async context =>
{
await context.Response.WriteAsync("End");
});
Terminal middleware ends the pipeline; no further Use or Run middleware will be executed. Commonly used to return quick responses during development.
Map
app.Map("/hello", appBranch =>
{
appBranch.Run(async context =>
{
await context.Response.WriteAsync("Hello branch");
});
});
Acts as a "branch by path" mechanism. It matches only the URL path prefix (e.g., /api/v1) and creates an independent middleware pipeline for the matched branch.
Encapsulate middleware logic in a separate class for use with UseMiddleware<>
public class MyMiddleware
{
private readonly RequestDelegate _next;
public MyMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext context)
{
// pre-process logic
await _next(context);
// post-process logic
}
}
// Register middleware in program.cs
app.UseMiddleware<MyMiddleware>();
Why doesn’t ASP.NET Core enforce a middleware interface like IMiddleware?
1.For flexibility and simplicity
ASP.NET Core supports two ways of registering middleware:
- Using inline delegates (e.g., app.Use(...))
- Using separate middleware classes (e.g., app.UseMiddleware())
// inline delegate style
app.Use(async (context, next) => {
// logic
await next();
});
// seperate class style
public class MyMiddleware {
public async Task InvokeAsync(HttpContext context) { ... }
}
If ASP.NET Core had enforced a strict interface like IMiddleware, it would have effectively ruled out the first, delegate-based style. This would go against ASP.NET Core’s minimalist and flexible design philosophy. Supporting both styles is a deliberate choice to make middleware development lightweight and developer-friendly.
2.Reflection-based Invocation of Invoke / InvokeAsync — Avoiding Structural Constraints
As long as the method signature is correct, the framework can invoke it—without requiring the developer to implement a specific interface. This approach avoids forcing a rigid middleware structure and provides greater flexibility in how middleware components are designed.
Routing middleware
The Routing middleware (UseRouting) is responsible for matching incoming HTTP requests to route endpoints defined in your application (e.g., controller actions, Razor Pages, minimal APIs).
It examines the request URL and determines which endpoint should handle it, then stores that information in HttpContext.
UseRouting() does not execute the endpoint — it only selects it.
The actual execution happens later via UseEndpoints() or MapControllers().
- Without calling UseRouting(), ASP.NET Core won’t know which controller or route to invoke — even if your controllers exist and routes are defined. You'll get a 404 for every request. Automatic execution fallback: If you don't explicitly call UseRouting(), it is still added early in the pipeline behind the scenes for minimal API setups — but in full applications, it must be explicitly used for routing to work properly.
- Must come before authorization: Because UseAuthorization() needs to know which endpoint is being accessed, UseRouting() must execute before it.
- Context preparation: It populates HttpContext.GetEndpoint() and HttpContext.Request.RouteValues, which downstream middleware and controllers rely on.
UseAuthentication and UseAuthorization
UseAuthentication() middleware:
- This middleware automatically reads the Authorization: Bearer header from incoming requests.
- If you have configured AddJwtBearer, it recognizes the Bearer token type, parses the JWT, verifies its signature and expiration.
- If the token is valid, it populates the HttpContext.User with claims such as sub, role, email, etc.
- If the token is invalid, User.Identity.IsAuthenticated will be false, but the request is not blocked at this stage.
UseAuthorization() middleware:
- This middleware handles authorization by checking the claims in HttpContext.User against policies like [Authorize] attributes or IAuthorizationFilter.
- If authorization fails, it automatically returns a 401 Unauthorized or 403 Forbidden response.
Additional notes:
- UseAuthentication is responsible for authentication (verifying user identity) but does not reject requests; it only sets the authentication state.
- UseAuthorization handles authorization (permission checks) and will block requests if the user is unauthorized. Both middlewares must be added to the request pipeline in the correct order: authentication first, then authorization.
Filters
Five main categories of filters in ASP.NET Core, classified by their execution stages:
- Authorization Filters: Execute early in the pipeline to determine whether the user is authorized to access the resource.
- Resource Filters: Run before and after the rest of the filter pipeline and action execution; used for caching, resource management, or short-circuiting the request.
- Action Filters: Execute code before and after the action method runs; commonly used for logging, validation, or modifying action parameters/results.
- Exception Filters: Handle exceptions thrown during action execution or later filters; used to implement centralized error handling.
- Result Filters: Run before and after the action result executes; often used to modify the response, such as adding headers or transforming the result.
The following diagram shows how filter types interact in the filter pipeline:
Definition: What is an Exception Filter
- Implements the
IExceptionFilter
orIAsyncExceptionFilter
interface; - Catches unhandled exceptions thrown during the execution of a controller or action;
- Can be used to generate unified error responses, log errors, and encapsulate exception structures.
Difference from Middleware:
Q: Must I use Exception Filters? Isn’t middleware better for handling exceptions?
A: If your project is purely an API, it’s recommended to use middleware for centralized exception handling. However, if you need special handling for certain controllers or need to access the ActionContext
during exceptions, Exception Filters are more suitable.
Core comparison: Exception Middleware vs. Exception Filters
Middleware is a system-level HTTP request pipeline controller that can do anything you want; filters are application-level lifecycle hooks that can only insert logic at the predefined “points” defined by Microsoft.
Why is it said that performing authorization in an ActionFilter is "too late"?
Performing authorization checks in an ActionFilter is considered "too late" because ActionFilters run after the routing and model binding stages, meaning the request has already been processed to some extent. By this time, critical resources may have already been allocated, and some business logic might have started executing. This can lead to unnecessary processing or potential security risks if unauthorized requests are only caught at this late stage.
In contrast, authorization is typically done earlier in the pipeline (e.g., via middleware or Authorization Filters), so that unauthorized requests can be rejected as soon as possible—before consuming more resources or exposing sensitive logic. This improves efficiency and enhances security.
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.