ASP.NET middleware

Middleware is a component in the app pipeline to perform some work on request and response, handing the modified request or response to the next middleware until the response is sent to the client.

A middleware might decide not to pass the request to the next middleware (short-circuiting the pipeline). When a middleware short-circuits, it's called a terminal middleware.

A middleware can be specified in-line as an anonymous method, or it can be defined in a reusable class. In ASP.NET we can add a middleware to the pipeline using Use or Run methods. The Run method is only for terminal middlewares hence there is no next argument available in its parameters.

Execution Order

It's important to understand how the request and response pass through middleware. When a request reaches the first middleware, it runs all the code until it reaches the await next.Invoke() method. If we want to modify the response header, for example changing the status code we need to do it before this line.

The reason for this limitation is that, once we call the next middleware, we have no idea what those middleware is going to do. Most probably the terminal middleware is going to write something to the response and start sending the response to the client. Once the first middleware, writes the response, ASP.NET starts sending the response to the client and the first thing it sends is the response headers. So it makes sense once we've already sent the response headers, we cannot change it anymore. However, since the response body is a memory stream we can append something to the response body, but we cannot replace the response body as the first chunk of the response body might have already been sent.

If we want to do something with the response header just before the response is going to be sent, we can use HttpResponse.OnStarting(() => {}) callback. Since this is a callback, we can use it safely in any middleware and ASP.NET will execute these functions just before sending the response hence we don't need to know if the next middleware is going to start the response or not.

Let's make it clear with an example:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.Use(async (context, next) =>
{
    context.Response.StatusCode = 1;
    await next.Invoke();
    await context.Response.WriteAsync("Middleware1\n");
});

app.Use(async (context, next) =>
{
    context.Response.StatusCode = 2;
    await next.Invoke();
    await context.Response.WriteAsync("Middleware2\n");
    // await context.Response.CompleteAsync();
});

app.Run(async context =>
{
    context.Response.StatusCode = 3;
    await context.Response.WriteAsync($"Terminal Middleware\n");
    // context.Response.StatusCode = 4;
});

app.Run();

In this example, there are three middleware, two normal middleware and one terminal middleware. Let's how the code is running here:

  1. First middleware, sets the status code to 1 and calls the next middleware.
  2. Second middleware, sets the status code to 2 and calls the next middleware.
  3. Third middleware, sets the status code to 3 and starts writing the response. At this stage, ASP.NET starts sending the response back to the client and the first thing that it sends is the response headers. So from this point, the response headers are unmodifiable. If we try to change any attributes of the response header, like the status code, it'll throw an error.
  4. Since we've reached the last middleware, the second middleware continues executing the rest of its code after await next.Invoke() . As ASP.NET uses a memory stream for sending the response back to the client, we are still able to add something to the response body, hence we can appended "Middleware2" string to the response body.
  5. In a similar way once the second middleware completes, the first middleware continues running the remaining code after await next.Invoke() and it appends "Middleware1" string to the response body.

The final response body for this call would be as below and the status code is 3:

Terminal Middleware
Middleware2
Middleware1

If we want to complete the response and prevent it reach to next middleware, we can use Response.CompleteAsync() method.