Anyone who writes a web application with ASP.NET Core uses at least one middleware component. But what exactly is middleware a middleware component, and how does it work? And why would I want to write one myself?
In this five-part series, I'll show what middleware components are, 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.
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 that can be found here.
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.
As depicted in the figure above, 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).
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.
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.
Kestrel is an HTTP server implementation that is used by default by every ASP.NET Core project. 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. For example, 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, on the other hand, is a rather simple web server that is concerned with receiving raw network requests, transforming them into an
HttpContext, and passing them on to the application.
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.
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 the event of a URL match and returning the requested resource directly, without the HTTP request having to go through the subsequent middleware components.
This is illustrated in the figure below. If, for example, an image is requested that is embedded in an HTLM 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.
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 named the minimal hosting model has been introduced, which represents a simplified form of the generic host.
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).
With the Generic Host, infrastructure & app configuration tasks were previously split into two files (
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).
A simple ASP.NET Core 6.0 application configuring the
StaticFileMiddleware would look like this.
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.
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 a huge number of built-in middleware components. And all of them share a convention: They can be added to the pipeline by using the corresponding extension method
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 is in contrast to 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.
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.
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 in turn is generated by Kestrel from a raw network request.
Further, we have seen that the order in which middleware components are registered is important for the processing logic.
It was also shown that every middleware can decide whether the subsequent delegate should be called or whether 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 have a closer look at what conventionalized middleware components are and how they differ from factory-based and in-line-defined middleware components.