Understanding and Customizing the ASP.NET Core Middleware Pipeline

Middleware is one of the most important concepts in ASP.NET Core because it plays a crucial role in handling requests and responses, allowing developers to control the request pipeline. But what is a request pipeline, and how can you better understand middleware in ASP.NET?

What is a Pipeline?

A pipeline is a series of steps that something passes through to reach its final destination or outcome. TTo better understand a pipeline, think of your recent trip. Before you reached your destination, you had to pack your bags, drive to the airport, check in, go through security, board the plane, and finally land at your destination. Each of these actions was necessary to complete your trip.

Similarly, in an ASP.NET application (or any application), when a request is sent, there are a series of checks that must happen, such as user authentication, route validation, logging user actions, etc. The request passes through a series of middleware components, with each one performing a specific task, much like each step of your trip.

But what exactly is middleware, and what is a middleware component?

What is Middleware?

Middleware is an application component that handles requests and responses as they pass through the request pipeline in an ASP.NET Core application. A middleware component is an individual unit within that pipeline, responsible for performing a specific task, such as logging, authentication, or error handling. Each request passes through multiple middleware components, which work together to process it.

For example, if you create an API to return a list of objects for authenticated users, each request follows a sequence:

  1. The user sends an HTTP request.
  2. It passes through logging middleware that records the request.
  3. Next, authentication middleware checks if the user is allowed access.
  4. Then, authorization middleware checks if the authenticated user has the right role
  5. If authenticated and authorized, the request reaches your API controller, which returns the list.
  6. The response then goes back through the middleware in reverse order.

In the last step you can see that middleware goes back thought the same pipeline, but in reverse order. That happens because each middleware component processes the response before it reaches the user.

For example, logging middleware tracks response details like status code and response time, while authentication middleware ensures only authorized users receive the response. Middleware can also add or modify response headers for security or caching, and adjust the response body if needed. If any errors occurred during processing, error-handling middleware catches and manages them before the response is sent.

But where do we find the middleware components to use in the pipeline? ASP.NET Core offers a variety of built-in components, ready to handle different tasks. You can also create custom middleware components if the default ones do not fullfill your application’s unique requirements.

Built-in Middleware Components

ASP.NET Core has many built-in middleware components that handle common tasks for web applications. You can easily add these components to your request pipeline to manage different parts of processing requests and responses. While there are a lot of them, let’s look at the top 5:

Authentication Middleware – This component is used to handle user authentication. It is used to check if the user is authenticated and manages the authentication schemes used in the application.

Authorization Middleware – This component is used to check if the authenticated user is allowed to access certain areas or resources in the application. Since this is related to role checks, it has to come after the authentication middleware component.

Exception Handling Middleware – As the name already indicates this middleware component is used capture and also handle the application exceptions. Here you basically specify how to handle errors globally for the application.

Static Files Middleware – This is another component that is used to serve static files such as HTML, CSS, JavaScript, and image files without further processing. This is commonly used for serving client-side resources.

Routing Middleware – This is a really important component because it makes the whole app work. Without this you are not able to navigate to different sections in your app. This middleware component matches incoming requests to route endpoints defined in your application. It basically allows you to control how requests are routed.

I have created this example in an ASP.NET Web API project, where I have the Program.cs. An example that uses all the middleware components above, would look like below:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllers(); // Add support for controllers (API)
builder.Services.AddAuthentication("Bearer") // Add authentication (JWT as an example)
    .AddJwtBearer("Bearer", options =>
    {
        options.Authority = "https://your-auth-server.com";
        options.Audience = "api1";
    });

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", policy =>
        policy.RequireRole("Admin"));
});

var app = builder.Build();

// 1. Exception Handling Middleware
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage(); // For development
}
else
{
    app.UseExceptionHandler("/Home/Error"); // Handle errors in production
    app.UseHsts(); // Enforce HTTP Strict Transport Security
}

// 2. Static Files Middleware
app.UseStaticFiles(); // To serve static files (e.g., HTML, CSS, JS)

// 3. Routing Middleware
app.UseRouting(); // Enables routing to controllers or other endpoints

// 4. Authentication Middleware
app.UseAuthentication(); // This validates user identity based on authentication schemes

// 5. Authorization Middleware
app.UseAuthorization(); // Enforces access control based on roles or policies

// 6. Map your endpoints (API controllers, MVC, Razor Pages, etc.)
app.MapControllers(); // For Web API or MVC controllers

// Run the application
app.Run();

Here are the top 5 middleware components, or at least what I consider the top 5, but .NET offers many other components, which you can explore at this link: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware#built-in-middleware

So, if a component is available, it’s easy to use. But what if you need a middleware component that .NET doesn’t offer? In ASP.NET, you can create your own custom middleware components. But how exactly do you do that?

Creating Your Own Custom Middleware Components

C# is an object-oriented programming language where classes are the fundamental building blocks for organizing code, so creating custom middleware involves working with classes. To create custom middleware, you need to follow these three simple steps:

  1. Create a new middleware class.
  2. Register the middleware in the pipeline.
  3. Add the middleware to the application pipeline.

To better understand the steps above, let’s create a custom middleware component that will log information and track the time of requests and responses. Based on these steps, we need to first create a class, which we’ll name RequestResponseLoggingMiddleware. Including “Middleware” in the name is not required, but it can be helpful for clarity. The most important thing is to choose a name that clearly reflects the middleware’s purpose, ensuring your code remains readable and maintainable.

public class RequestResponseLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<RequestResponseLoggingMiddleware> _logger;

    public RequestResponseLoggingMiddleware(RequestDelegate next, ILogger<RequestResponseLoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task Invoke(HttpContext context)
    {
        // Log when the middleware is triggered
        Console.WriteLine("Middleware Triggered: Request incoming");

        _logger.LogInformation("Request Path: {Path}", context.Request.Path);
        var startTime = DateTime.UtcNow;

        // Call the next middleware in the pipeline
        await _next(context);

        var executionTime = DateTime.UtcNow - startTime;
        _logger.LogInformation("Response Status Code: {StatusCode}", context.Response.StatusCode);
        _logger.LogInformation("Execution Time: {ExecutionTime} ms", executionTime.TotalMilliseconds);
    }
}

This RequestResponseLoggingMiddleware class is a custom ASP.NET Core middleware designed to log information about incoming HTTP requests and outgoing responses. It captures and logs the request path and headers before passing the request down the pipeline. It also ogs the response status code and calculates the total execution time after the request is processed by subsequent middleware components.

The Invoke method is essential in custom middleware because it defines how the middleware interacts with the incoming request and outgoing response. It is where the middleware performs its specific tasks (such as logging, in this case) and passes the request to the next component in the pipeline using _next(context). The method is called automatically by ASP.NET Core, and it’s what enables the middleware to be part of the request/response pipeline. Without the Invoke method, the middleware would not be able to process or forward requests.

Next, we need to add the middleware to the pipeline in a clean and reusable way, for that I will create an extension method for IApplicationBuilder. We have to create an extension method so we can then use the IApplicationBuilder instance in Program.cs or Startup.cs and simply call the method name. The code would looks like below:

public static class RequestResponseLoggingMiddlewareExtensions
{
    public static IApplicationBuilder UseRequestResponseLogging(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<RequestResponseLoggingMiddleware>();
    }
}

This code adds a helper tool to make it easier to use the request-logging feature in an ASP.NET Core app. It lets developers add the logging tool to their app with a simple, readable line of code. This makes it quicker and less confusing to set up the logging system when starting the app.

Lastly, we need to register this middleware component in the middleware pipeline. For that you can use either Program.cs or Startup.cs depending on how you have configured the application.

var builder = WebApplication.CreateBuilder(args);

// Add services to the container
builder.Services.AddControllers();
builder.Services.AddAuthentication(); // Add authentication service if needed
builder.Services.AddAuthorization();

var app = builder.Build();

// Add your custom middleware before other middleware components
app.UseRequestResponseLogging();

// Other middlewares
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();

// Map the controller endpoints
app.MapControllers();

app.Run();

Now, I am just going to test it and show you the results. You can get the same code in this github repository. I ran this code and this is the result

Middleware Triggered: Request incoming
info: MiddlewareApp.Demo.RequestResponseLoggingMiddleware[0]
      Request Path: /api/values
info: MiddlewareApp.Demo.RequestResponseLoggingMiddleware[0]
      Response Status Code: 200
info: MiddlewareApp.Demo.RequestResponseLoggingMiddleware[0]
      Execution Time: 0.794 ms

The full source code is available here: https://github.com/etrupja/MiddlewareApp.Demo

In the example above, I added UseRequestResponseLogging before all the other components and UseAuthentication() before UseAuthorization(). Does the order matter? The short answer is: Yes. But, why?

Why Does Order Matter?

The order of middleware in the request pipeline is really important because each middleware component processes the request in sequence, and this affects how the application behaves. Middleware components interact with both the requests and the responses, and their order determines how those interactions happen.

For example, if UseAuthorization() is placed before UseAuthentication(), the authorization middleware would try to check what permissions a user has without confirming the user’s identity, leading to errors. Also, once the request is processed by a controller or endpoint, the response travels back through the middleware in reverse order. Middleware registered later in the pipeline will handle the response before middleware registered earlier. This means that components like logging, which might need to log response details, should be placed early in the pipeline to catch both the request and the response.

In our example if we were to place a logging middleware at the end of the pipeline, it might miss important details because the response is already partially processed by the time it logs anything.

Many middleware components depend on the output of earlier components. Routing must come before any logic that needs to know which controller or endpoint will handle the request. Authentication must happen before Authorization because the app needs to know who the user is before checking what they are allowed to do.

We did put our custom pipeline in the beginning becuase as a general unwritten rule global middleware should go early in the pipeline to capture request/response details.

Conclusion

In ASP.NET Core, middleware components play a vital role in handling requests and responses, with their order in the pipeline determining how the application processes each step. Understanding both built-in and custom middleware allows developers to effectively manage tasks like logging, authentication, and error handling. By carefully structuring the middleware pipeline, you ensure that requests flow smoothly and securely through the application.


Enjoyed this post? Subscribe to my YouTube channel for more great content. Your support is much appreciated. Thank you!


Check out my Udemy profile for more great content and exclusive learning resources! Thank you for your support.
Ervis Trupja - Udemy



Enjoyed this blog post? Share it with your friends and help spread the word! Don't keep all this knowledge to yourself.