Understanding Middleware in ASP.NET Core


Web applications can quickly become a mess, especially when handling repetitive tasks like user authentication, logging, and error management for every request. Fortunately, .NET's middleware offers a structured solution to these challenges.
The Basics of ASP.NET Core Middleware
Middleware in ASP.NET Core is a building block of the HTTP request pipeline. But what exactly is the HTTP request pipeline?
Imagine a series of interconnected stations, each performing a specific task. When a user sends a request to your ASP.NET Core web application, it doesn't immediately reach your application's core logic. Instead, it enters this pipeline. Each station in the pipeline, or each middleware component, has the opportunity to inspect, modify, or even short-circuit the request.
Here is how the Request flows:
- Request Arrives at the Web Server (IIS or Kestrel):
- When a user sends a request, it first reaches your web server, typically either IIS (for Windows servers) or Kestrel (a cross-platform web server built into ASP.NET Core).
- HttpContext is Generated:
- The web server then passes the request to the ASP.NET Core runtime. Here,
HttpContext
object is created from the request. This object holds all the details of the request, such as headers, the request body, and URL parameters.
- Request Enters the Middleware Pipeline:
- The
HttpContext
object now enters the middleware pipeline, where a series of middleware process the request.
- Middleware Components Process the Request:
Each middleware component in the pipeline performs a specific task, in a defined order.
For example, an authentication middleware might check user login details from the
HttpContext
.A logging middleware logs the information from the request.
An error handling middleware might manage any errors that occur during the request.
- Routing Determines the Endpoint:
- After the middleware components have processed the request, the routing middleware determines which part of your application (endpoint) should handle it. This could be a controller action in your API or a page in your web application.
- Endpoint Executes and generates a response:
- The selected endpoint then runs, using the information from the
HttpContext
and a response is generated.
- Response Sent to User via Middleware (in Reverse Order):
- The generated response now travels back through the middleware pipeline, but in reverse order. This allows each middleware component to perform any necessary post-processing tasks before the web server (IIS or Kestrel) sends the final response back to the user.
Why Middleware?
Imagine building a Web API that requires a number of actions to be performed for every incoming request. You might need to handle user authentication and authorization, implement rate limiting, provide centralized exception handling, and maintain logs. Without a centralized solution, you'll likely end up with duplicated code and a difficult-to-maintain application.
Consider a common scenario: logging request information. Without middleware, you might have logging code scattered throughout your API endpoints:
public IActionResult GetData(int id)
{
var requestPath = HttpContext.Request.Path;
_logger.LogInformation($"Request to {requestPath} with id: {id}");
// logic to get data
return Ok(data);
}
public IActionResult CreateData(Data data)
{
var requestPath = HttpContext.Request.Path;
_logger.LogInformation($"Request to {requestPath}");
// logic to create data
return CreatedAtAction(nameof(GetData), new { id = data.Id }, data);
}
As you add more endpoints, this duplicated logging code becomes increasingly difficult to manage. If you need to change the logging behavior (e.g. add more details, change the log format), you'd have to modify it in multiple places.
To address this duplication issue, middleware effectively centralizes this common logging logic into a reusable component, applied to every request. This centralized approach simplifies development, enhances maintainability, and ensures consistency.
The Middleware Pipeline: Ordering Matters
The order in which middleware components are added to the pipeline is important. For instance, authentication middleware should typically come before authorization middleware. If the order is reversed, authorization might be attempted before the user's identity is established, leading to unexpected behavior. Similarly, error handling middleware should be placed early in the pipeline to catch exceptions thrown by subsequent components. If placed after other middleware, it might miss exceptions from earlier stages, resulting in unhandled errors.
As the diagram above illustrates, the sequence of middleware components directly affects the request/response flow. In this example, if the 'Endpoint Middleware' handles the request and generates a response, the request never reaches the 'Authentication' and 'Authorization' middleware.
Short-Circuiting the pipeline
Middleware can "short-circuit" the pipeline, stopping further processing and sending a response directly. This is crucial for security and performance.
For example, authentication middleware might return a 401 Unauthorized response, preventing unauthenticated requests from reaching your application. Similarly, error handling middleware placed early in the pipeline ensures that exceptions from other components are caught, preventing unhandled errors and potential security risks.
The diagram above demonstrates how authentication and authorization middleware interact and how short-circuiting can occur. Authentication middleware verifies user credentials, and if valid, associates the authenticated user with the current request. Authorization middleware then checks if the user is authorized to access the requested resource. If the user is not allowed, the authorization middleware will short-circuit the pipeline and generate an Unauthorized response, preventing the request from reaching the application's core logic.
Building a Custom Exception Handling Middleware with Logging
Middleware's way of managing requests gives you a lot of flexibility, and one great use is for handling errors and logging them yourself. This custom approach in your ASP.NET Core application can make things smoother for users and easier to figure out when things go wrong, beyond what the default settings offer.
Step 1: Creating the Custom Middleware
We define a custom middleware class, ExceptionHandlingMiddleware
. This middleware follows the standard ASP.NET Core pattern, requiring a constructor that takes a RequestDelegate
(representing the next middleware in the pipeline) and an InvokeAsync
method that handles the request. We also inject an ILogger
instance to enable logging of exceptions.
public class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex, _logger);
}
}
private static async Task HandleExceptionAsync(HttpContext context, Exception exception, ILogger<ExceptionHandlingMiddleware> logger)
{
HttpStatusCode statusCode;
string message;
switch (exception)
{
case AuthenticationException ex:
statusCode = HttpStatusCode.Unauthorized;
message = "Authentication failed.";
logger.LogWarning(exception, "Authentication failure.");
break;
case UnauthorizedAccessException ex:
statusCode = HttpStatusCode.Unauthorized;
message = "You are not authorized to access this resource.";
logger.LogWarning(exception, "Unauthorized access attempt.");
break;
case KeyNotFoundException _:
statusCode = HttpStatusCode.NotFound;
message = "The requested resource was not found.";
logger.LogWarning(exception, "Resource not found.");
break;
case ArgumentException _:
statusCode = HttpStatusCode.BadRequest;
message = "Invalid request parameters.";
logger.LogWarning(exception, "Invalid request.");
break;
default:
statusCode = HttpStatusCode.InternalServerError;
message = "An unexpected error occurred. Please try again later.";
logger.LogError(exception, "Unhandled exception occurred.");
break;
}
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)statusCode;
var errorResponse = new { StatusCode = (int)statusCode, Message = message };
var jsonResponse = JsonSerializer.Serialize(errorResponse);
await context.Response.WriteAsync(jsonResponse);
}
}
The InvokeAsync
method operates by attempting to execute the subsequent middleware in the pipeline (_next(context)
). If any exception is thrown during this process, the catch
block is executed. Here, the exception is logged using the injected ILogger
. The HandleExceptionAsync
method then formats an error response as JSON, which is sent back to the client.
Step 2: Registering the Middleware
To integrate the ExceptionHandlingMiddleware
into the request pipeline, it is registered in the Startup.cs
or Program.cs
(.Net core 6.0 and later) using app.UseMiddleware<ExceptionHandlingMiddleware>()
. Because the goal is to capture all unhandled exceptions that might occur during the processing of a request, it's crucial to register this middleware early in the pipeline. This ensures that it wraps the execution of subsequent middleware components, allowing it to intercept any exceptions before they reach the default error handling mechanisms.
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseMiddleware<ExceptionHandlingMiddleware>();
app.UseAuthorization();
app.MapControllers();
app.Run();
Step 3: Testing the Middleware
To demonstrate the functionality of the ExceptionHandlingMiddleware
, an API endpoint can be created to intentionally trigger an exception:
[ApiController]
[Route("api/[controller]")]
public class ErrorController : ControllerBase
{
[HttpGet("trigger")]
public IActionResult TriggerError()
{
throw new InvalidOperationException("This is a test exception from the ErrorController.");
}
}
When a request is made to the /api/error/trigger
endpoint, the InvalidOperationException
will be thrown. Our ExceptionHandlingMiddleware
will catch this exception, log it on the server, and return a JSON response to the client with a 500 Internal Server Error status code and a generic error message. The server-side logs will contain the detailed exception information.
The Power of Middleware: A Final Look
Understanding and utilizing ASP.NET Core middleware is essential for building effective web applications. From its role in the request pipeline to the creation of custom components like our exception handler, middleware offers powerful tools for managing request flow, handling errors, and implementing cross-cutting concerns. By embracing middleware, you gain significant control over your application's behavior and architecture.
Happy Coding!!
Subscribe to my newsletter
Read articles from Anil gurau directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
