blog post image
Andrew Lock avatar

Andrew Lock

~10 min read

Building a middleware pipeline with WebApplication

Exploring .NET 6 - Part 4

In my previous post, I looked at the code behind WebApplicationBuilder, including some of its helper classes like ConfigureHostBuilder and BootstrapHostBuilder. At the end of the post, we had created an instance of the WebApplicationBuilder and called Build() to create a WebApplication. In this post, we look at a bit of the code behind WebApplication, and focus on how the middleware and endpoints are configured.

The WebApplication: a brief recap

This post follows on directly from the previous post about WebApplicationBuilder, as well as the introduction to WebApplication, so I suggest you start with those posts if you haven't already read them. That said, I'm not going to be as deep in the code in this post compared to the previous one!

As I described in my previous post, WebApplicationBuilder is where you do most of your application configuration, while WebApplication is used for three separate things:

  • It's where you configure your middleware pipeline, because it implements IApplicationBuilder.
  • It's where you configure your endpoints using MapGet(), MapRazorPages() etc, because it implements IEndpointRouteBuilder.
  • It's what you run to actually start your application by calling Run() because it implements IHost.

At the end of the previous post, we saw that when you call Build() on a WebApplicationBuilder instance, a private instance of the generic host Host is created, and passed to the WebApplication constructor. It's from this point that this post continues, to give a little insight into WebApplication before we start looking at middleware pipelines

WebApplication: a relatively thin wrapper around three types.

In comparison to the WebApplicationBuilder constructor, the WebApplication constructor is relatively simple:

public sealed class WebApplication : IHost, IApplicationBuilder, IEndpointRouteBuilder, IAsyncDisposable
{
    private readonly IHost _host;
    private readonly List<EndpointDataSource> _dataSources = new();
    internal IDictionary<string, object?> Properties => ApplicationBuilder.Properties;

    internal static string GlobalEndpointRouteBuilderKey = "__GlobalEndpointRouteBuilder";

    internal WebApplication(IHost host)
    {
        _host = host;
        ApplicationBuilder = new ApplicationBuilder(host.Services);
        Properties[GlobalEndpointRouteBuilderKey] = this;
    }
    // ...
}

The constructor and field initializers do 4 primary things:

  • Save the provided Host in the _host field. This is the same Host type as when you use the generic host directly, as in the ASP.NET Core 3.x/5.
  • Create a new list of EndpointDataSource. These are used to configure the endpoints in your application, including Razor Pages, Controllers, API endpoints, and the new "minimal" APIs.
  • Create a new instance of ApplicationBuilder. This is used to build the middleware pipeline, and is essentially the same type that's been used since version 1.0!
  • Set the __GlobalEndpointRouteBuilder property on the ApplicationBuilder. This property is used to "communicate" to other middleware that we're using the new WebApplication, which has some different defaults, as you'll see a bit later.

WebApplication mostly delegates the implementation of the IHost, IApplicationBuilder, and IEndpointRouteBuilder to its private properties, and implements them explicitly. For example, for IApplicationBuilder, much of the implementation is delegated to the ApplicationBuilder created in the constructor:

IDictionary<string, object?> IApplicationBuilder.Properties => ApplicationBuilder.Properties;

IServiceProvider IApplicationBuilder.ApplicationServices
{
    get => ApplicationBuilder.ApplicationServices;
    set => ApplicationBuilder.ApplicationServices = value;
}

IApplicationBuilder IApplicationBuilder.Use(Func<RequestDelegate, RequestDelegate> middleware)
{
    ApplicationBuilder.Use(middleware);
    return this;
}

Where things get interesting, is when you call RunAsync(), which builds the middleware pipeline.

Starting the WebApplication and building the middleware pipeline

The standard way to start your application is to call app.Run() on your WebApplication. This calls into WebApplication.RunAsync, which in turn calls an IHost extension method, HostingAbstractionsHostExtensions.RunAsync(), which is the same one that you'd use with the generic host in 3.x/5. This in turn calls IHost.StartAsync, which starts the complex startup interactions I've written about previously.

Sequence diagram for Host.StartAsync()

As shown in the diagram above, the Host runs the IHostedServices registered in the application as part of the startup sequence. As of the "great re-platforming" in ASP.NET Core 3.x, the web server also runs as an IHostedService, so this is when the middleware pipeline is built and Kestrel starts listening.

GenericWebHostService is where the middleware pipeline is built, and finally is passed to Kestrel to run your app. There's nothing different in this process for WebApplication compared to the generic Host, so rather than dive through the (extensive!) code, we'll take a look at how WebApplication sets up the middleware pipeline instead.

WebApplication's middleware pipeline

One of the big differences with WebApplication compared to the generic host is that WebApplication sets up various middleware by default. In the previous post I showed that WebApplicationBuilder calls GenericHostBuilderExtensions.ConfigureWebHostDefaults(). This is commonly used with the generic host too, and sets up several defaults:

In addition to these, WebApplicationBuilder sets up extra middleware. This is where the ConfigureApplication method I mentioned in the previous post comes in. Depending on how you configure your WebApplication, ConfigureApplication adds additional middleware to your pipeline.

The code for this is a little complex, as it requires handling multiple edge cases. Rather than try and keep track of the code in too much detail, we'll look at some examples instead, and see what the resulting middleware pipeline looks like.

The empty pipeline

We'll start with the most basic (and somewhat pointless) application, in which we don't add any extra middleware or endpoints to the application:

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

app.Run();

This setup is the most basic of configurations, and results in a middleware pipeline that contains:

  • HostFilteringMiddleware
  • DeveloperExceptionPageMiddleware
  • An "empty" middleware built from WebApplication's ApplicationBuilder.

something like the following:

The WebApplication adds the DeveloperExceptionPageMiddleware by default

The HostFilteringMiddleware is added thanks to the hidden call to ConfigureWebHostDefaults(), as described in the last section. The ForwardedHeadersMiddleware wasn't added as the environment variable wasn't added.

The DeveloperExceptionPage is now added automatically when you're running in the Development environment, adding the following to the start of every middleware pipeline:

if (context.HostingEnvironment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

The final piece of middleware in the pipeline is built using the middleware added directly to the WebApplication's ApplicationBuilder in Program.cs. In this example we didn't add any, so we essentially add an "empty" pipeline inside the middleware.

Obviously an application with just the default middleware isn't much use, so let's do the basics and add some middleware to WebApplication.

A pipeline with extra middleware

In this example, we add some additional middleware to the pipeline: the static file middleware, and the HTTPS redirection middleware:

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

// Add some additional middleware
app.UseHttpsRedirection();
app.UseStaticFiles();

app.Run();

With this setup we still have the same three core middleware as in the previous section, but the WebApplication.ApplicationBuilder pipeline now contains two additional middleware

  • HostFilteringMiddleware
  • DeveloperExceptionPageMiddleware
  • The WebApplication.ApplicationBuilder pipeline, containing
    • HttpsRedirectionMiddleware
    • StaticFilesMiddleware

so now we have something that looks like this:

The WebApplication.ApplicationBuilder pipeline includes the middleware added directly to the WebApplication

We still don't have any endpoints yet, so in the next example we implement a "Hello World!" endpoint:

A "Hello world" pipeline

In the following example, we add a single endpoint for the home page which returns the text "Hello World!" into the example I showed in the previous case:

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

app.UseHttpsRedirection();
app.UseStaticFiles();

// Add a single endpoint
app.MapGet("/", () => "Hello World!");
app.Run();

Adding an endpoint using MapGet() adds an entry to the WebApplication's collection of EndpointDataSources, which causes the ConfigureApplication to automatically add additional middleware:

  • HostFilteringMiddleware
  • DeveloperExceptionPageMiddleware
  • EndpointRoutingMiddleware (AKA RoutingMiddleware)
  • The WebApplication.ApplicationBuilder pipeline, containing
    • HttpsRedirectionMiddleware
    • StaticFilesMiddleware
  • EndpointMiddleware

Notice that the RoutingMiddleware is automatically added to the middleware pipeline before the start of the pipeline defined in Program.cs, and the EndpointMiddleware is automatically added to the pipeline at the end. The resulting pipeline looks something like the following:

The WebApplication has added the the RoutingMiddleware and EndpointMiddleware to the pipeline

I find it very interesting that WebApplication hides away the routing middleware from users. The fact that the RoutingMiddleware and EndpointMiddleware are so closely related, and that they impose strict ordering requirements (e.g. the AuthorizationMiddleware must be placed between them), makes them a relatively difficult concept for newcomers to grasp. With .NET 6 and WebApplication, there's fewer things for users to worry about!

It's not all rosy though. Some middleware generally assumes that it will be called before UseRouting(). For example, the ExceptionHandlerMiddleware, RewriterMiddleware, StatusCodePagesMiddleware all had to be updated to handle this new pattern.

But what if you need the RoutingMiddleware to be at a specific point in the pipeline? Maybe you have middleware that needs to be before the RoutingMiddleware, for example?

A pipeline with UseRouting()

In the next example, we explicitly add the EndpointRoutingMiddleware to the WebApplication.ApplicationBuilder pipeline by calling UseRouting(). This is closer to what you'd expect to see in a .NET 3.x/5 app, where UseRouting() is placed in the middle of your pipeline

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

app.UseHttpsRedirection();
app.UseStaticFiles();

// EndpointRoutingMiddleware in the middle of the pipeline.
app.UseRouting();

app.MapGet("/", () => "Hello World!");
app.Run();

As you might expect, the resulting pipeline contains all the same middleware as before, but arranged in a slightly different order:

  • HostFilteringMiddleware
  • DeveloperExceptionPageMiddleware
  • The WebApplication.ApplicationBuilder pipeline, containing
    • HttpsRedirectionMiddleware
    • StaticFilesMiddleware
    • EndpointRoutingMiddleware (RoutingMiddleware)
  • EndpointMiddleware

Which can be visualised something like this:

The middleware pipeline contains the RoutingMiddleware in the WebApplication.ApplicationBuilder

There's an interesting interplay between the "main" middleware pipeline, and the WebApplication.ApplicationBuilder pipeline, where the WebApplicationBuilder has to be careful to preserve the overall order when UseRouting() is specified. Hopefully this won't often be required, but when it is, you should still be able to use WebApplicationBuilder if you wish, rather than being forced to fall back to the generic host.

Finally, lets look at what happens if you add the other side of endpoint routing, the EndpointMiddleware.

A pipeline with UseEndpoints()

As I mentioned earlier, endpoint routing works with a pair of middleware, the EndpointRoutingMiddleware (AKA RoutingMiddleware) which selects the endpoint, and the EndpointMiddleware, which executes the endpoint. The EndpointMiddleware is typically added to the pipeline at the end, by calling UseEndpoints(), but with WebApplicationBuilder it's added automatically. Let's see what happens if we also add it explicitly.

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

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.MapGet("/", () => "Hello World!");

// Add the EndpointMiddleware explicitly, and add a new endpoint
app.UseEndpoints(x => 
{
    x.MapGet("/ping", () => "pong")
});

app.Run();

With the above configuration, you end up with the following:

  • HostFilteringMiddleware
  • DeveloperExceptionPageMiddleware
  • The WebApplication.ApplicationBuilder pipeline, containing
    • HttpsRedirectionMiddleware
    • StaticFilesMiddleware,
    • EndpointRoutingMiddleware (RoutingMiddleware), and
    • EndpointMiddleware
  • EndpointMiddleware

If you read the above list closely, you'll see that the EndpointMiddleware appears twice! That's not a bug, but rather a consequence of the fact that the WebApplicationBuilder can't tell if the EndpointMiddleware was added to the WebApplication.ApplicationBuilder pipeline. Luckily the "outer" middleware is completely benign.

The middleware pipeline contains two instances of the EndpointMiddleware

You might also notice that I registered endpoints both directly on WebApplication and also in the UseEndpoints() call. However, those endpoints are all registered with the WebApplication's _dataSources, thanks to some tricky twiddling with properties in WebApplicationBuilder. The end result is that the second middleware is never called.

That covers most of the edge cases related to building the middleware pipeline in WebApplication. I think it's quite interesting to see the changes made here, though as I've mentioned before, I wish there was a better delineation between middleware and endpoints in WebApplication. For example, I wish it looked more like this:

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

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

// I wish the "Endpoints" property was a thing
app.Endpoints.MapGet("/", () => "Hello World!");
app.Endpoints.MapGet("/ping", () => "pong");

app.Run();

But as it stands, there's no denying that WebApplication and WebApplicationBuilder provide a simpler API than the generic Host implementation they wrap. Hopefully that makes it easier to teach to newcomers!

Summary

In this post we saw how the new "minimal hosting" WebApplication builds the middleware pipeline for your application. We started by looking at the constructor of the WebAppliation, to get a feel for how it works, and then looked at some example pipelines. For each pipeline I showed the code you write, and the resulting middleware pipeline. This showed how WebApplication automatically (conditionally) adds the DeveloperExceptionPageMiddleware at the start of your pipeline, and wraps your pipeline in the routing middleware. Overall, this can simplify middleware ordering issues, but it's good to be aware of what's happening.

Implementing the minimal hosting API required quite a few changes to middleware to accommodate the new pattern. In the next post, we look at some other things in .NET Core that had to change!

Andrew Lock | .Net Escapades
Want an email when
there's new posts?