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.
![](https://i.imgur.com/Loie3mZ.png)
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.
![](https://docs.microsoft.com/en-us/media/logos/logo-ms-social.png)
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.
![](https://i.imgur.com/Fn4hUqR.png)
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.
![](https://i.imgur.com/Fz9RqEK.png)
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.
![](https://i.imgur.com/XjGADzh.png)
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
.
![](https://i.imgur.com/0JTaxLd.png)
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.
![](https://i.imgur.com/3VlqGGX.png)
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
![](https://docs.microsoft.com/en-us/media/logos/logo-ms-social.png)
![](https://images.manning.com/360/480/resize/book/7/f64fb4a-6b86-4926-8f14-5e35e43c8f7d/Lock-ANCore-2ed-HI.png)
![](https://andrewlock.net/content/images/cover.jpg)