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 revisit last year’s post about ASP.NET hosting.
Prior to RC2, an ASP.NET Core application was a class library. DNX – the execution environment at that time – was responsible for providing support for project.json, loading assemblies and invoking the ASP.NET hosting libraries, which in turn would find a Startup class to configure and start the application. Just for completeness, we used to have something like this on the commands section of project.json:
"web": "Microsoft.AspNet.Hosting --server Microsoft.AspNet.Server.WebListener --server.urls http://localhost:5000"
With the release of RC2, DNX and the related tools were replaced by the .NET CLI and, as such, ASP.NET Core applications became regular console applications. Instead of using the hosting libraries as the entry point and have them discover our application, we now call into ASP.NET libraries from our Main method (the common entry point) to configure the web application and fire up ASP.NET. Here’s an example of such a Main method:
public static void Main(string[] args) { var host = new WebHostBuilder() .UseKestrel() .UseStartup<Startup>() .Build(); host.Run(); }
The code is straightforward: we configure the server (Kestrel, in this case) and the Startup class (this now has to be done explicitly) to build the web host. Then we run it, blocking the main thread until the application shuts down. For the remainder of this post (and the following) I’ll go through the ASP.NET code to see what’s happening under the covers when we configure and run the web host.
Before moving further it’s worth doing a quick recap on some terminology:
- RequestDelegate – represents the handling of a request. It’s a function that gets a context and returns a Task that completes when the request has been processed. This simple delegate actually represents the whole application.
public delegate Task RequestDelegate(HttpContext context);
- Middleware – a component that participates on request processing by inspecting, routing, or modifying request and/or response messages. A simplistic view of a middleware is a Func<RequestDelegate, RequestDelegate>, i.e. a means of composing around a given request delegate. This composition approach replaces the event-driven model found on previous versions of ASP.NET.
- Startup class – the class where you configure the services and middlewares of your application. The configuration methods are discovered by convention, as described on ASP.NET documentation. You can also check this post for a more detailed explanation.
The interface of WebHostBuilder is rather simple, as illustrated below. Configuring the builder is mostly about configuring services for the DI infrastructure.
public interface IWebHostBuilder { IWebHost Build(); IWebHostBuilder ConfigureServices(Action<IServiceCollection> configureServices); IWebHostBuilder UseSetting(string key, string value); // ... } public class WebHostBuilder : IWebHostBuilder { private readonly List<Action<IServiceCollection>> _configureServicesDelegates; public IWebHostBuilder ConfigureServices(Action<IServiceCollection> configureServices) { _configureServicesDelegates.Add(configureServices); return this; } // ... }
Most of the builder configurations done on the Main method are exposed via extension methods on the corresponding packages. For instance, the UseKestrel method is found on the Kestrel server package and looks like this:
public static IWebHostBuilder UseKestrel(this IWebHostBuilder hostBuilder) { return hostBuilder.ConfigureServices(services => { services.AddTransient<IConfigureOptions<KestrelServerOptions>, KestrelServerOptionsSetup>(); services.AddSingleton<IServer, KestrelServer>(); }); }
Notice the registration of IServer. This is (as expected) a required service that represents the server and will be used later on to link incoming requests to the application. If we were to use another server implementation, it would likely register its own IServer class via a similar extension method.
The WebHostBuilder.Build method is where everything is assembled. It has two main responsibilities:
- Configure and register the services needed for hosting.
- Create and initialize the WebHost.
public IWebHost Build() { var hostingServices = BuildHostingServices(); var hostingContainer = hostingServices.BuildServiceProvider(); var host = new WebHost(hostingServices, hostingContainer, _options, _config); host.Initialize(); return host; }
The BuildHostingServices method is where hosting-related configuration takes place. First, the IHostingEnvironment is configured. This service can be used from this moment on to get information about the physical environment where the application is hosted, such as the root path for static content.
_options = new WebHostOptions(_config); var appEnvironment = PlatformServices.Default.Application; var contentRootPath = ResolveContentRootPath(_options.ContentRootPath, appEnvironment.ApplicationBasePath); var applicationName = _options.ApplicationName ?? appEnvironment.ApplicationName; _hostingEnvironment.Initialize(applicationName, contentRootPath, _options); var services = new ServiceCollection(); // (1) services.AddSingleton(_hostingEnvironment);
Note the creation of the service collection (1) that is used to create the base DI service provider for the WebHost (on the Build method shown before).
The next relevant action is the registration of some important built-in services that will be used for application configuration and request processing. We’ll go through the usage of these services later on.
services.AddLogging(); services.AddTransient<IApplicationBuilderFactory, ApplicationBuilderFactory>(); services.AddTransient<IHttpContextFactory, HttpContextFactory>(); services.AddOptions();
- IApplicationBuilderFactory – allows create of the application builder that is used by the Startup class to configure middlewares.
- IHttpContextFactory – create the HttpContext for an incoming request.
- AddOptions – adds support for the options configuration pattern.
The last step is to execute the service configuration delegates that were externally registered in the WebHostBuilder. This is the case of the delegates registered by UseKestrel and UseStartup methods, as shown before.
private readonly List<Action<IServiceCollection>> _configureServicesDelegates; // ... foreach (var configureServices in _configureServicesDelegates) { configureServices(services); }
At this point all the base configurations and services required by the host are in place. The Build method can now create a WebHost instance and initialize it. This where all the application-specific configurations are triggered and it will be the subject of my next post. Stay tuned!