Introduction to ASP.NET Core middleware - part 1

Introduction to ASP.NET Core middleware - part 1

Introduction

Anyone who writes a web application with ASP.NET Core uses at least one middleware component. But what exactly is a middleware component, and how does it work? And why would I want to write one myself?

In this five-part series, I'll explain middleware components, how they work, and how you can develop one yourself. I'll also highlight testability and give some examples.


Articles in this series

Part 1 - Introduction to ASP.NET Core middleware
Part 2 - Writing inline ASP.NET Core middleware using lambda expressions
Part 3 - Writing convention-based ASP.NET Core middleware
Part 4 - Writing factory-based ASP.NET Core middleware
Part 5 - Testing ASP.NET Core middleware


What is middleware?

The term middleware cannot be clearly defined because it is used differently depending on its context. However, in the context of an ASP.NET Core-based web application, the term middleware defines a component that is part of the application-specific HTTP request-response pipeline. Every HTTP request travels through this pipeline to generate an HTTP response.

HTTP request-response pipeline

Each of these middleware components fulfills precisely one task. This task can consist of delivering static files, such as CSS and JavaScript, which the browser reads and interprets. It can also include tasks in the areas of authentication and authorization, data compression, and more.

The ASP.NET Core pipeline follows the Single Responsible Principle (SRP), which states that every module, class, or function should have responsibility over a single part of that program's functionality, and it should encapsulate that part.

ASP.NET Core comes with a huge list of ready-to-use built-in components found here.

ASP.NET Core Middleware
Learn about ASP.NET Core middleware and the request pipeline.

HTTP Pipeline

When multiple middleware components are chained together, they form the HTTP pipeline, which is then traversed by each HTTP request. The concatenation of the components is realized by delegates, each pointing to the subsequent component.

A request-response pipeline formed by delegates

As the figure above shows, an HTTP request is passed from middleware to middleware component. Eventually, an HTTP response is generated and sent back to the caller via the pipeline (which, on its way back again, traverses each component).

HTTP Context

Each HTTP request, respectively HTTP response, is encapsulated in an object of type HttpContext. This object contains a list of HTTP headers and the request and response body (among other information). Any middleware can modify both the HTTP request and the HTTP response and perform actions based on the state of this HttpContext. Thus, the pipeline is bi-directional.

The HttpContext object is created by Kestrel, the ASP.NET Core web server. This usually receives the raw network request from a reverse proxy, constructs a C# representation from it (in the form of the HttpContext type), and forwards it to the application, respectively, the middleware pipeline.

A reverse proxy, Kestrel, and the HTTP context

Kestrel is an HTTP server implementation used by every ASP.NET Core project by default. This server supports HTTPs, HTTP/2, and Unix sockets for accelerated information exchange with reverse proxies such as NGINX.

Although using a reverse proxy is not a requirement, it is recommended for operation in a productive environment. This is because a reverse proxy can take on various tasks that Kestrel does not support, such as caching content, accelerating TLS, firewalling, or even load distribution to several instances behind the proxy. Popular examples of reverse proxies are NGINX, Apache, and IIS.

Kestrel and the middleware pipeline

Kestrel, on the other hand, is a relatively simple web server that is concerned with receiving raw network requests, transforming them into an HttpContext, and passing them on to the application.

Demonstration example

Now that the stage is set let's have a closer look at how an HTTP request is served by taking the built-in StaticFileMiddleware as an example.

As the name suggests, the task of this middleware is to deliver static content. This includes cascading style sheets, JavaScript, images, static HTML pages, and everything located in the web root under the configurable folder {content-root}/wwwroot.

StaticFileMiddleware

The StaticFileMiddleware will reduce the resource requirements (CPU, memory, IOPS) of an ASP.NET Core application by delivering static content without involving the downstream middleware components. It fulfills this task by short-circuiting the pipeline in case of a URL match and returning the requested resource directly without the HTTP request going through the subsequent middleware components.

This is illustrated in the figure below. If, for example, an image is requested that is embedded in an HTML document, it will be served by the middleware, and the pipeline will be short-circuited. No further processing will take place by any subsequent components. In case the requested document cannot be resolved by the StaticFileMiddleware, the request will be passed on.

A short-circuited pipeline prevents subsequent components from running.

The fact that middleware components can short-circuit or terminate the processing of the pipeline implies an important aspect! That is the order of the middleware registration matters! And it is decisive for the application logic.

☝🏻 The order of middleware registration matters and is decisive for the application logic.

To illustrate, imagine adding an additional built-in middleware called HttpsRedirectionMiddleware after the StaticFileMiddleware. Which, to state the obvious, enforces HTTPs. This setup would only redirect to HTTPs in case the StaticFileMiddleware is not able to resolve a requested document, which is very unlikely what you want.

Where & how is middleware getting configured?

The core of every ASP.NET Core application is the so-called host (defined by the IHost interface). It is generated when the application starts and includes the HTTP server implementation (Kestrel), but also logging functionality, dependency injection services, and the middleware pipeline.

Two hosting models are already known from ASP.NET Core 3.1 and 5.0: the .NET Generic Host and the ASP.NET Core Web Host. Whereby the Web Host is only required for special cases in which backward compatibility is required.

With ASP.NET Core 6.0, a third model, the minimal hosting model, was introduced. This model represents a simplified form of the generic host.

public sealed class WebApplication : IHost, IDisposable, IApplicationBuilder, IEndpointRouteBuilder, IAsyncDisposable

Minimal Hosting Model

Looking at the amount of implemented interfaces gives an idea that the WebApplication class is a complex type that would be time-consuming to instantiate manually. This type is, therefore, instantiated via a builder (see Builder pattern).

var builder = WebApplication.CreateBuilder();
var app = builder.Builder();
// ...
app.Run()

Creating and starting a web application

With the Generic Host, infrastructure & app configuration tasks were previously split into two files (Program.cs and Startup.cs).

Now, everything is in one file. If this is too confusing and you want more structure in your application, I'd recommend following the example below (credits go to Andrew Lock).

var builder = WebApplication.CreateBuilder(args);

ConfigureConfiguration(builder.Configuration);
ConfigureServices(builder.Services);

var app = builder.Build();

ConfigureMiddleware(app, app.Services);
ConfigureEndpoints(app, app.Services);

app.Run();

void ConfigureConfiguration(ConfigurationManager configuration) {} 
void ConfigureServices(IServiceCollection services) {}
void ConfigureMiddleware(IApplicationBuilder app, IServiceProvider services) {}
void ConfigureEndpoints(IEndpointRouteBuilder app, IServiceProvider services) {}

A recommended way to structure an ASP.NET Core 6 application

A simple ASP.NET Core 6.0 application configuring the StaticFileMiddleware would look like this.

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

ConfigureMiddleware(app, app.Services);

app.Run();

void ConfigureMiddleware(IApplicationBuilder app, IServiceProvider services) 
{
    app.MapGet("/", () => "Hello World");
    app.UseStaticFiles()
}

A minimal ASP.NET Core 6 application

In the example, an HTTP request directed to / is answered with Hello World". The call to MapGet() defines a terminating middleware that short-circuits the pipeline and returns a response.

The StaticFileMiddleware is never called for a request to the URL /. If the URL differs, the HTTP context is passed on to the StaticFileMiddleware, which was added to the pipeline using the UseStaticFiles() extension method.

As mentioned in the beginning, ASP.NET Core provides many built-in middleware components. All of them share a convention: They can be added to the pipeline by using the corresponding extension method Use*, e.g. UseStaticFiles(), ...

Registering middleware with the DI container

You may have noticed that no explicit registration of the StaticFileMiddleware class with the DI container was necessary.

This is because the StaticFileMiddleware is a conventionalized middleware, which is instantiated via introspection (reflection) when the application starts. This type of middleware contrasts factory-based middleware, which needs to implement the IMiddleware interface and requires explicit registration with the DI container.

☝🏻 Part 2 of this series will go into more detail on inline-defined, conventionalized & factory-based middleware.

The IHost interface prescribes the property Services which is of type IServiceProvider. It is implemented by the WebApplication class and is used to register services (including factory-based middleware). Finally, the method UseMiddleware<TMiddleware>() adds it to the request-response pipeline.

var builder = WebApplication.CreateBuilder(args);
ConfigureServices(builder.Services);

var app = builder.Build();
ConfigureMiddleware(app, app.Services);

app.Run();

void ConfigureServices(IServiceCollection services) 
{
    services.AddScoped<MyMiddleware>();
}
void ConfigureMiddleware(IApplicationBuilder app, IServiceProvider services) 
{
    app.MapGet("/", () => "Hello World");
    app.UseMiddleware<MyMiddleware>();
}

Registering a middleware

Conclusion

As we have seen, the HTTP pipeline is a concatenation of delegates, where each corresponds to a middleware component. These components transport, modify and process the HTTP context, which Kestrel generates from a raw network request.

Further, we have seen that the order in which middleware components are registered is essential for the processing logic.

It was also shown that every middleware can decide whether the subsequent delegate should be called or if the pipeline should be terminated (short circuit).

Finally, it was shown that the conventionalized middleware components do not have to be explicitly registered on the DI container.

In the second part of this series, we'll examine conventionalized middleware components and how they differ from factory-based and in-line-defined middleware components.

Further reading

ASP.NET Core Middleware
Learn about ASP.NET Core middleware and the request pipeline.
ASP.NET Core in Action, Second Edition
A hands-on primer to building cross-platform web applications with your C# and .NET skills. Go from basic HTTP concepts to advanced framework customization.
Andrew Lock | .NET Escapades
Hi, my name is Andrew, or ‘Sock’ to most people. This blog is where I share my experiences as I journey into ASP.NET Core.
Introduction to ASP.NET Core middleware - part 1
This article explains the high-level concepts of an ASP.NET Core middleware and is the first in a series of three articles.
Writing inline ASP.NET Core middleware using lambda expressions - part 2
This article is part of a series about ASP.NET Core middleware and explains how inline-middleware can be written using lambda expressions
Writing convention-based ASP.NET Core middleware - part 3
This article is part of a series that explains how to develop convention-based middleware components with ASP.NET Core
Writing factory-based ASP.NET Core middleware - part 4
This article is part of a series that explains how to develop factory-based ASP.NET Core middleware
Testing ASP.NET Core middleware - part 5
Introduction This is the last in a series of posts that explains how ASP.NET Core middleware components can be written. Articles in this series Part 1 - Introduction to ASP.NET Core middleware Part 2 - Writing inline ASP.NET Core middleware using lambda expressions Part 3 - Writing