On a previous post we saw how DNX’s Application Host is responsible for adding support for the new project structure, namely assembly loading and command handling. The last step on that layer is invoking the application layer itself, which in our case is ASP.NET.
In order to find ASP.NET’s entry point, DNX Application Host uses the same logic used on the Managed Entry Point, i.e. it tries to find a Program class with a Main method. In this case, the Program class on Microsoft.AspNet.Hosting is the entry point. This layer is responsible for configuring the ASP.NET application and, ultimately, start the web server. The code on the entry point is pretty straight forward:
public void Main(string[] args) { // ... var builder = new ConfigurationBuilder(appBasePath); // ... builder.AddEnvironmentVariables(); builder.AddCommandLine(args); var config = builder.Build(); var host = new WebHostBuilder(_serviceProvider, config).Build(); using (host.Start()) { Console.WriteLine("Started"); var appShutdownService = host.ApplicationServices.GetRequiredService(); Console.CancelKeyPress += delegate { appShutdownService.RequestShutdown(); }; appShutdownService.ShutdownRequested.WaitHandle.WaitOne(); } }
The first task is to create a Configuration object, loading configuration data from environment variables and command line. The configuration system is completely different in the version of ASP.NET. Web.config is gone; configuration settings are loaded from different sources, such as environment variables and JSON files, and made available via a common interface. In our case, we can see in debug that the server configuration parameters were loaded from the command line.
The next step is to create a IHostingEngine via the WebHostBuilder and start it. This is where all the core logic of configuring the application resides (more on this soon). Finally, since we’re self hosting, a callback is registered to request application shutdown when CTRL + C is pressed and the host waits for that event.
Startup class
Before going into more details, lets just recap a few concepts:
-
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 and IApplicationBuilder – the application bootstrap is convention-based. By default, ASP.NET hosting searches the main assembly of the application for a class named Startup and then for a method named Configure that has the signature illustrated below. This method is passed an application builder which can be used to register middlewares. For the remainder of this post i’ll use the two simple middlewares shown below.
public class Startup { public void Configure(IApplicationBuilder app) { app.Use(next => httpContext => { httpContext.Response.Headers.Append("Debug", "Hello world!"); return next(httpContext); }); // Run just ignores the "next" paramater (end of the pipeline) app.Run(ctx => ctx.Response.WriteAsync("Hello ASP.NET 5 world! Time: " + DateTime.Now)); } }
For more information, I’ve explained these components a while ago on my Hello World posts; also I found this post that explains the “pipeline” details pretty well.
WebHostBuilder
Back to the code, the WebHostBuilder class is responsible for creating an IHostingEngine that can be used to start the application/server. Following the builder pattern, the class has a set of methods to configure the hosting engine that will be built, namely to explicitly define the startup and server types:
When none of these methods are used, the class uses the built-in default logic, such as getting the server name from configuration and trying to find a Startup class on the application assembly. This is actually the case on the self hosting scenario, as we saw above. On other hosting scenarios, some explicit configurations may take place.
Going into the Build method, we can see that the first step is to register some hosting services that will be required further on:
services.AddTransient<IStartupLoader, StartupLoader>(); services.AddTransient<IServerLoader, ServerLoader>(); services.AddTransient<IApplicationBuilderFactory, ApplicationBuilderFactory>(); services.AddTransient<IHttpContextFactory, HttpContextFactory>(); services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
For now I’d like to highlight the following of those services:
-
IStartupLoader – used find the Startup type and the configuration methods (more on this later).
-
IServerLoader – user to find the IServerFactory that will start the server. The implementation is pretty much a search for the appropriate type.
-
IHttpContextAccessor – this is pretty much a test- and DI-friendly replacement for the old, nasty HttpContext.Current. The implementation uses logical call context or similar to keep the HttpContext reference. By the way, despite going by the same name, this HttpContext is completely new and much slimmer than the old counterpart.
These and other already configured services are passed to HostingEngine and the server and startup types are set accordingly to the builder methods. Note the usage of the configuration to get the server assembly as a fallback.
var startupLoader = hostingContainer.GetRequiredService(); // ... var engine = new HostingEngine(hostingServices, startupLoader, _config); // Only one of these should be set, but they are used in priority engine.ServerFactory = _serverFactory; engine.ServerFactoryLocation = _config.Get(ServerKey) ?? _serverFactoryLocation; // Only one of these should be set, but they are used in priority engine.Startup = _startup; engine.StartupType = _startupType; engine.StartupAssemblyName = _config.Get(ApplicationKey) ?? appEnvironment.ApplicationName; return engine;
HostingEngine
The HostingEngine is the class responsible for actually starting the server and defining how requests are handed to the application. When the Start method is invoked the following major steps are executed in order to get the server/application running:
1. Find Startup class and configuration methods
In this step, the IStartupLoader mentioned before is used. There are two parts: the first is to lookup the Startup class, if not explicitly defined on the WebHostBuilder; the second is to lookup the Configure method on that class.
if (Startup != null) { return; } if (StartupType == null) { // ... StartupType = _startupLoader.FindStartupType(StartupAssemblyName, diagnosticTypeMessages); // ... } Startup = _startupLoader.LoadMethods(StartupType, diagnosticMessages);
On the FindStartupType method is worth mentioning that the actual startup type can be named Startup or StartupXXXX, where XXXX is the environment name (e.g. StartupProduction). This allows you to have different startup classes per deployment environment.
The LoadMethods method is responsible for finding the Configure method on the startup class. In addition it searches for an optional ConfigureServices method, which can be used to register services on the DI infrastructure (I’ve deliberately omitted this method up until now). These methods can also have environment dependent names: ConfigureXXXX and ConfigureXXXXServices, where XXXX is the environment name. The following structure is returned to represent these configuration methods:
public class StartupMethods { // ... public Func ConfigureServicesDelegate { get; } public Action ConfigureDelegate { get; } }
2. Configure application services on DI infrastructure
Having the configuration methods, ASP.NET allows the application to register its services and possibly return a different IServiceProvider:
_applicationServices = Startup.ConfigureServicesDelegate(_applicationServiceCollection);
When the startup class doesn’t define a ConfigureServices method, there’s a default that simply builds the IServiceProvider from the current service collection.
3. Load and initialize the IServerFactory
if (ServerFactory == null) { ServerFactory = _applicationServices .GetRequiredService() .LoadServerFactory(ServerFactoryLocation); } _serverInstance = ServerFactory.Initialize(_config);
Recall that on our example the ServerFactoryLocation was supplied via command line to the WebHostBuilder in the first place.
4. Create the application middlewares chain
From the framework point-of-view, creating the middleware chain/pipeline is simple:
var builderFactory = _applicationServices.GetRequiredService(); var builder = builderFactory.CreateBuilder(_serverInstance); var configure = Startup.ConfigureDelegate; // ... configure(builder);
However, I’d like to highlight a few aspects around IApplicationBuilder. This interface is very simplistic when it comes to registering middlewares:
public interface IApplicationBuilder { // ... IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware); RequestDelegate Build(); }
On the example Startup class shown before, the Use method is invoked directly. There’s, however, a UseMiddleware() extension method that’s worth mentioning, because it includes the convention to find a RequestDelegate on the middleware type:
public static IApplicationBuilder UseMiddleware(this IApplicationBuilder builder, Type middleware, params object[] args) { var applicationServices = builder.ApplicationServices; return builder.Use(next => { var instance = ActivatorUtilities.CreateInstance(builder.ApplicationServices, middleware, new[] { next }.Concat(args).ToArray()); (1) var methodinfo = middleware.GetMethod("Invoke", BindingFlags.Instance | BindingFlags.Public); (2) var parameters = methodinfo.GetParameters(); if (parameters[0].ParameterType != typeof(HttpContext)) (3) { throw new Exception("Middleware Invoke method must take first argument of HttpContext"); } if (parameters.Length == 1) { return (RequestDelegate)methodinfo.CreateDelegate(typeof(RequestDelegate), instance); } // ... }); }
The extension method registers an “inline middleware” that instantiates the supplied middleware type (1) and lookups a method named Invoke (2) which gets an HttpContext as the first parameter (3). This method is used as the middleware’s RequestDelegate. An important detail is that the “next” RequestDelegate is passed as an argument when creating an instance of the middleware, allowing it to eventually invoke the next element on the pipeline. An example of a middleware compliant with this convention is:
public class GotchaMiddleware { private readonly RequestDelegate _next; public GotchaMiddleware(RequestDelegate next) { _next = next; } public async Task Invoke(HttpContext httpContext) { await _next(httpContext); await httpContext.Response.WriteAsync(" GOTCHA!"); } }
5. Build the final RequestDelegate
After the ConfigureDelegate is invoked, the ApplicationBuilder contains an ordered list of middlewares, as shown below. This screenshot corresponds to the Startup class shown before with an additional call to UseMiddleware().
The first middleware is added internally to handle per-request DI stuff; the others are the ones registered by the application. We can match the anonymous functions type names shown on the debugger to the code on the Startup class: one inside the Configure method, one inside the Run extension method and another inside the UseMiddleware extension method. This corresponds to the three middlewares registered on the Startup class.
The next task of HostingEngine is to invoke the ApplicationBuilder Build method in order to create the final RequestDelegate. 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.
The code on the Build method does exactly that: reverses the middleware list and invokes them one by one, passing the current RequestDelegate. To bootstrap the configuration, HostingEngine 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; }
6. Start the server
Finally, the server is started using the IServerFactory previously created and the HostingEngine returns a disposable that allows shutting down the server.(used by the Main method all the way up to the start of this post). To start the server, the HostingEngine supplies a callback that handles incoming requests. Naturally, this callback uses the RequestDelegate built on the previous step.
var server = ServerFactory.Start(_serverInstance, async features => { var httpContext = // ... // ... await application(httpContext); }); _applicationLifetime.NotifyStarted(); return new Disposable(() => { _applicationLifetime.NotifyStopping(); server.Dispose(); _applicationLifetime.NotifyStopped(); });
I’ll cover the request handling callback on the next post (the last on this series). Stay tuned!
Pingback: ASP.NET Core hosting (revisited) – Part I | Luís Gonçalves