ASP.NET Core: Beware - Singleton may not be singleton
ASP.NET Core in production: Graceful shutdown and reacting to aborted requests

ASP.NET Core in production: Take back control of your web app

With ASP.NET Core the setup of a new MVC or a Web API project is essentially a 2-liner. The standard setup you can find in most tutorials is ideal for demos, workshops or simple web apps but is insufficient for complex applications.

The standard setup looks as follows:

WebHost
.CreateDefaultBuilder()
.UseStartup<Startup>()
.Build()
.Run();

The rest of the configuration of the web app is usually made in Startup.

The problem with the standard approach is that the WebHost controls your entire web application, i.e. it dictates the app life cycle, sets up dependency injection framework, starts and stops your processes and if the WebHost is shutting down (or crashes) then your web application is down as well. Especially the stopping of the app is more of a crash than a graceful shutdown.

A web application is usually more than just a bunch of endpoints. Depending on your requirements you may need a more complex startup, perhaps consisting of multiple steps like

  • setting up logging first
  • pulling configuration
  • filling caches
  • processing of (temp) data from previous run
  • starting some tasks like listening on the file system or for pulling the requests from a message queue because HTTP is not the only gateway to the world
  • etc.

Only after all preparation steps are made the app is ready to open the HTTP endpoints to accept new requests, i.e. to start the WebHost. The web app may run for weeks or month so you may want to restart it (automatically) multiple times to reconfigure the ASP.NET Core pipeline without killing the whole process. And, if the app has to be stopped then you probably want to close the endpoints first, give running requests some time to finish, process the data in buffers or temp files, log some statistics and only then to stop the application.

The first step to get these features is to get control over dependency injection (DI) managing your components. Giving full control of the DI to the ASP.NET Core runtime means that you can't bootstrap your processes before starting the WebHost and if it is stopped then the DI container incl. all your components is disposed of as well. This makes a graceful shutdown very hard. 

In the following examples I'm using Autofac because it is one of the feature-richest and most mature dependency injection frameworks available for .NET. Especially, with Autofac v4.6.1 and Autofac.Extensions.DependencyInjection is it very easy to achieve the desired setup.

Example of a more complex startup so it is easier to get the idea what I 'm talking about: (pseudo code)

var builder = new ContainerBuilder();
// register all dependencies specific to your app
// not necessarilly specific to ASP.NET ...
using (var container = builder.Build())
{
    // assume, depending on some configurations
    // you need to add further dependencies
    var myConfig = container.Resolve<MyConfiguratio>();

    // Adding further dependencies on a child scope
    // can be repeated multiple times if required
    using (var appScope =
        container.BeginLifetimeScope(appScopeBuilder => RegisterFurtherDeps(myConfig)))
    {
        // All methods should do their work in child scopes of "appScope"
        // which should be disposed eventually to free the resources.

        // For example, check file system permissions, databases etc.
        CheckPreconditions(appScope);
        CleanupTheMessFromPreviousRun(appScope);
        StartSomeKindOfTasks(appScope);

        await StartWebHostAsync(appScope);

        // when we are here then that means the endpoints are closed
        WaitForPendingRequests(maxWaitTime: Timespan.FromSeconds(5));
        Cleanup();
        LogStatistics();
    }
}

....

public async Task StartWebHostAsync(ILifetimeScope appScope)
{
    // The WebHost gets its own scope and can do with it what ever it wants,
    // like "polluting" it with some ASP.NET stuff :)
    using (var webHostScope =
        appScope.BeginLifetimeScope(builder => RegisterAspNetSpecificStuff(builder)))
    {
        await WebHost
        .CreateDefaultBuilder()
        .UseStartup<Startup>()
         // This method does not exist but you get the point
        .UseThisDependencyInjectionScope(webHostScope) 
         .Build()
        .RunAsync();
    }
}

 

After a lengthly motivational part let's get to the real code.

In this demo we will setup an MVC web and restart the WebHost on button-click just to proof that the application stays alive even if the web server goes down. 

 At first we set up a DI container and register a class MySingleton as a singleton. This class will be the proof that it is not going to be recreated every time the WebHost restarts.

private static async Task StartWebAppAsync()
{
    var builder = new ContainerBuilder();
    builder.RegisterType<MySingleton>().AsSelf().SingleInstance();
    
    using (var container = builder.Build())
    {
        while (true)
       {
           await StartWebServerAsync(container);
       }
   }
}

The configuration of the WebHost differs just in one point: the instantiation of the class Startup is delegated to Autofac. Because of that our Startup is getting ILifetimeScope via constructor injection.

private static async Task StartWebServerAsync(ILifetimeScope scope)
{
    using (var webHostScope = scope.BeginLifetimeScope(
        builder => builder.RegisterType<Startup>().AsSelf()))
   {
       await WebHost
        .CreateDefaultBuilder()
        .UseStartup<Startup>()
        .ConfigureServices(services =>
            services.AddTransient(provider => webHostScope.Resolve<Startup>()))
        .Build()
        .RunAsync();
   }
}

Note: If your Startup has some ASP.NET specific dependencies, say IHostingEnvironment and IConfiguration then you have to pull them out of IServiceProvider and pass them to a factory created by Autofac.

public Startup(ILifetimeScope webHostScope, 
    IHostingEnvironment hostingEnvironment,
    IConfiguration configuration)
{
    ...
}

...
await WebHost
    ...
    .ConfigureServices(services => services.AddTransient(provider =>
    {
        var hostingEnv = provider.GetRequiredService<IHostingEnvironment>();
        var config = provider.GetRequiredService<IConfiguration>();
        var factory = webHostScope.Resolve<Func<IHostingEnvironment, IConfiguration, Startup>>();          return factory(hostingEnv, config);
    }))
    ...

Ok, back on track.  

In the Startup we copy the DI registrations from IServiceCollection to our DI container by using the method Populate and save the newly created DI scope in a variable so it can be disposed when the ASP.NET part is shutting down.

public IServiceProvider ConfigureServices(IServiceCollection services)
{
    // register ASP.NET Core stuff
    services.AddMvc();
    _aspNetScope = _webHostScope.BeginLifetimeScope(builder => builder.Populate(services));     return new AutofacServiceProvider(_aspNetScope);
}
public void Configure(IApplicationBuilder app, IApplicationLifetime appLifetime)
{
    app.UseMvc();
    appLifetime.ApplicationStopped.Register(() => _aspNetScope.Dispose());
}

 

Your MVC application is now set up properly.

Want to try it out? Download the sources from Github, launch the mini MVC app with dotnet run, go to the url printed in the console window and start restarting the WebHost :)

ASP.NET Core - Take back the control of your web app

 

P.S.: Getting hold of the DI is just one but not the only one aspect of keeping your app under control. But that's a topic for another day...

Comments

Feed You can follow this conversation by subscribing to the comment feed for this post.

The comments to this entry are closed.