ASP.NET Core hosting (revisited) – Part II

Last year I did a series of posts following ASP.NET Core execution in depth. At that time the framework was still named ASP.NET 5, I was using betas 5/6 and DNX (.NET Execution Environment) was still around. A lot has changed since then, namely the usage of .NET CLI instead of DNX. Today I’ll continue my previous post on hosting and somehow revisit last year’s post about ASP.NET request processing.

The last post ended up right before the WebHost is initialized. This is where ASP.NET configures the application services and middlewares in order to build the final RequestDelegate. Finally, when started, the host fires up the registered server (IServer), linking the incoming requests to the RequestDelegate. Lets go through these steps.

The initialization of WebHost is where the Startup class plays its role. Internally, the startup logic is represented by the IStartup interface, depicted below:

public interface IStartup
{
    IServiceProvider ConfigureServices(IServiceCollection services);
    void Configure(IApplicationBuilder app);
}

This interface mimics the two application startup actions: 1) configure services on DI and 2) configure the middlewares. On the Main method, we invoked UseStartup<T>; if you take a look at the method’s source code you’ll see that it actually registers an implementation of IStartup that resolves the two actions by convention, by looking up methods on the supplied startup class.

Anyway, the first actions on the WebHost initialization are all about gathering the required services and configuring the application services (the following lines are a bit scattered on the source; I condensed the most relevant parts here):

_startup = _hostingServiceProvider.GetRequiredService<IStartup>();
_applicationServices = _startup.ConfigureServices(_applicationServiceCollection);
Server = _applicationServices.GetRequiredService<IServer>();
var builderFactory = _applicationServices.GetRequiredService<IApplicationBuilderFactory>();
IApplicationBuilder builder = builderFactory.CreateBuilder(Server.Features);

Notice how the host gets required services that were previously configured by the host builder and our code on the Main method.

At this point the application services are configured on the DI infrastructure and the host has an application builder ready to start configuring middlewares. There are three main players on that stage:

  • IApplicationBuilder – used to register middlewares. The interface is very simplistic:
    public interface IApplicationBuilder
    {
        // ...
        RequestDelegate Build();
        IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);
    }
  • The Startup.Configure method – where the application configures its middlewares. To that end, it gets an IApplicationBuilder as an argument, as shown below:
    public class Startup
    {
        public void Configure(IApplicationBuilder app) { ... }
    }
  • Startup filters – classes that implement IStartupFilter, allowing configurations to be done on the IApplicationBuilder before the Startup.Configure method is invoked. The interface is as follows:
    public interface IStartupFilter
    {
        Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next);
    }

The only usage of these filters I found on ASP.NET source is to register a middleware that handles the per-request lifetime on the DI infrastructure. It’s important to ensure that this middleware executes before application middlwares so that the latter can use per-request services.

The construction of the aplication RequestDelegate starts exactly by applying startup filters. The bootstrap Action<IApplicationBuilder> is the Startup.Configure method and filters successively wrap around the previous action. Note that filters are applied in reverse order of registration, so that the first to be registered is actually the outermost wrapper (the first to execute).

var startupFilters = _applicationServices.GetService<IEnumerable<IStartupFilter>>();
Action<IApplicationBuilder> configure = _startup.Configure;
foreach (var filter in startupFilters.Reverse())
{
    configure = filter.Configure(configure);
}

Having this composition of middleware configuration actions, it’s time to actually execute them on the IApplicationBuilder and then build the RequestDelegate.

configure(builder);
return builder.Build();

After the configure delegate is executed, the application builder contains and ordered list of middlewares, as shown below. For this example I have two middlewares on my Startup class; the other is the aforementioned middleware that handles per-request DI.

middlewares

Recall that a middleware is a function that composes a RequestDelegate around another (the next one in the chain). Therefore, the final RequestDelegate must be built from the last middleware to the first, as illustrated below. This ensures that the first middleware to be registered is actually the outermost wrapper (the first to execute).

Untitled.png

The application builder’s Build method does exactly that: reverse the middlewares list and invoke them one by one, passing the current RequestDelegate. To bootstrap the configuration, it uses a default RequestDelegate that returns an HTTP 404 status code, ensuring a default response when no other middleware handles the request.

public RequestDelegate Build()
{
    RequestDelegate app = context =>
    {
        context.Response.StatusCode = 404;
        return Task.FromResult(0);
    };
    foreach (var component in _components.Reverse())
    {
        app = component(app);
    }
    return app;
}

At this point the WebHost is initialized and we have a RequestDelegate that represents the whole application. When the host is started (its Run method is invoked on the console application) it’s time to bring the bring the server implementation in and have it deliver requests to the RequestDelegate. To that end, one last component is used: IHttpApplication.

public interface IHttpApplication<TContext>
{
    TContext CreateContext(IFeatureCollection contextFeatures);
    Task ProcessRequestAsync(TContext context);
    void DisposeContext(TContext context, Exception exception);
}

This interface is the glue between the server and our application. This is visible on the IServer interface:

public interface IServer : IDisposable
{
    void Start<TContext>(IHttpApplication<TContext> application);
    // ...
}

Just by analyzing these two interfaces we can imagine a skeleton of what’s happening under the covers, probably with some actions in between:

  1. When the server is started, it is given an IHttpApplication.
  2. When a new request arrives on the server, it asks the application  to create a context for the request.
  3. The context is supplied to the application for request processing. This means that the context must contain everything the application needs to handle the corresponding request.
  4. After the request is done, the server asks the application to dispose of the previously created context.

So, what’s that IFeatureCollection thing on CreateContext? Features are fine-grained representations of the capabilities exposed by the server for the current request. There are feature interfaces to represent the HTTP connection, the request, the response, and so on. You can take a look at all the features on the HttpAbstractions repository.

If you recall, the RequestDelegate gets an HttpContext and returns a Task. It is the hosting layer’s responsibility to wrap the low-level feature interfaces exposed by the server in a higher-level, strongly-typed HttpContext. This is done via the built-in implementation of IHttpApplication:

private readonly RequestDelegate _application;
private readonly IHttpContextFactory _httpContextFactory;

public Context CreateContext(IFeatureCollection contextFeatures)
{
    var httpContext = _httpContextFactory.Create(contextFeatures);
    // ...
    return new Context
    {
        HttpContext = httpContext,
        // ...
    };
}

public Task ProcessRequestAsync(Context context)
{
    return _application(context.HttpContext);
}

Going back a bit, when we start a WebHost, it actually starts the underlying server passing in the IHttpApplication and everything is ready for request processing. When a request comes in and the server invokes IHttpApplication.ProcessRequestAsync, the HttpContext is passed into the application RequestDelegate and we’re immediately executing middlewares’ code. There’s literally nothing else on the way of the request! Pretty slim, right?

execution

As a final note – and in case you’re wondering – it’s worth mentioning that HttpContext itself is an abstraction for applications to build on. The IHttpContextFactory implementation creates instances of DefaultHttpContext, which in turn uses DefaultHttpRequest and DefaultHttpResponse. These classes contain the logic of mapping feature interfaces into the high-level APIs.

The two sets of abstractions – feature interfaces and HTTP abstractions – allow applications to be hosted on different servers and different servers to be used without being tied to one specific hosting model, as illustrated below.

abstractions.PNG

And that’s it for this series. I revisited the execution of an ASP.NET Core application, starting from configuration via WebHostBuilder up to the moment where a request is handled to the application RequestDelegate. Hope this helps!

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s