DotNet Core 2.1 and the Generic Host

We’ve been writing a lot of backend services using DotNet Core recently; these services don’t expose any http endpoints and as such we’ve not been using ASP.net or the WebHost (or WebHostBuilder) it provides.

The WebHost class manages the lifetime of the web application; it does a few things for you:

  • Sensible defaults and a convention based approach to configuration; allows you to combine several ordered configuration sources into a single set of key/value pairs.
  • Sets up an IOC container for you.
  • Manages a thread pool and automatically starts any IHostedService instances from the IOC container.
  • Configure a server (Kestrel) and request processing pipeline.

In our console applications we still want configuration from multiple sources with sensible defaults, an IOC container and the ability to run background services but we need to set them up ourselves (prior to DotNet Core 2.1).

Setting the above utilities up isn’t very difficult (or even verbose) in DotNet Core but it’s a chunk of boilerplate code that we were copying from service to service, our Program.class files all looked something like this:

// Note that we use Serilog for our Logging.
internal class Program
{
    private static void Main(string[] args)
    {
        var container = ConfigureServices();
        container.GetService<IBusControl>().Start();
        container.GetService<MyHostedService>().StartAsync();
    }

    private static ServiceProvider ConfigureServices()
    {
        // Get the environment name
        var environmentName = Environment.GetEnvironmentVariable("ENVIRONMENT");

        // Create the configuration object
        var configuration = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("appsettings.json", false)
            .AddJsonFile($"appsettings.{environmentName}.json", true, true)
            .AddEnvironmentVariables()
            .Build();
            
        // Configure Serilog
        Log.Logger = new LoggerConfiguration()
            .ReadFrom.Configuration(configuration)
            .CreateLogger();

        // Build the IOC container
        var builder = new ServiceCollection();
        builder.AddSingleton(Log.Logger)
            .Configure<DbConfiguration>(configuration.GetSection("Db"))
            .Configure<RabbitCfg>(context.Configuration.GetSection("RabbitMq"))
            .AddDatabase()
            .AddSingleton<MyHostedService>()
            .AddMassTransit();

        return builder.BuildServiceProvider();
    }
}

Dotnet Core 2.1 introduces the HostBuilder and GenericHost classes which simplify the creation of Console Applications and provide some of the same goodies that the WebHost provides in ASP.

The GenericHost provides the same functionality as the WebHost sans the web specific functionality (i.e. no Kestrel or request pipeline).

Our Program.class now begins to look like this:

class Program
{
    static async Task Main(string[] args)
    {
        await new HostBuilder()
            // Allows us to use env variables in ConfigureAppConfiguration
            .ConfigureHostConfiguration(builder => builder.AddEnvironmentVariables())
            // Create the configuration object
            .ConfigureAppConfiguration((context, builder) => builder
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json")
                .AddJsonFile($"appsettings.{context.HostingEnvironment.EnvironmentName}.json", true, true)
                .AddEnvironmentVariables()
            )
            // Configure Serilog
            .UseSerilog((context, configuration) => configuration
                .ReadFrom.Configuration(context.Configuration)
            )
            // Build the IOC container
            .ConfigureServices((context, collection) => collection
                .AddSingleton(Log.Logger)
                .Configure<DbConfiguration>(context.Configuration.GetSection("Db"))
                .Configure<RabbitCfg>(context.Configuration.GetSection("RabbitMq"))
                .AddDatabase()
                .AddSingleton<MyHostedService>()
                .AddMassTransit()
            )
            .RunConsoleAsync();
    }
}

Although it’s not a massive reduction in lines of code we are now using out of the box framework functionality which will hopefully be much easier to maintain in the future.