ASP.NET Core 2.1 introduced the ASP.NET Core Generic Host for non-HTTP scenarios. In standard HTTP ASP.NET Core applications, you configure your app using the WebHostBuilder in ASP.NET Core, but for non-HTTP scenarios (e.g. messaging apps, background tasks) you use the generic HostBuilder.

In this post I describe some of the similarities and differences between the standard ASP.NET Core WebHostBuilder used to build HTTP endpoints, and the HostBuilder used to build non-HTTP services. I discuss the fact that they use similar, but completely separate abstractions, and how that fact will impact you if you try and take code written for a standard ASP.NET Core application, and reuse it with a generic host.

If the generic host is new to you, I recommend checking out Steve Gordon's introductory post. For more detail on the nuts-and-bolts, take a look at the documentation.

How does HostBuilder differ from WebHostBuilder?

ASP.NET Core is used primarily to build HTTP endpoints using the Kestrel web server. A WebHostBuilder defines the configuration, logging, and dependency injection (DI) for your application, as well as the actual HTTP endpoint behaviour. By default, the templates use the CreateDefaultBuilder extension method on WebHost in program.cs:

public class Program
{
    public static void Main(string[] args)
    {
        CreateWebHostBuilder(args).Build().Run();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>();
}

This extension method sets up the default configuration providers and logging providers for your app. The UseStartup<T>() extension sets the Startup class where you define the DI services and your app's middleware pipeline.

Generic hosted services have some aspects in common with standard ASP.NET Core apps, and some differences.

Hosted services can use the same configuration, logging, and dependency injection infrastructure as HTTP ASP.NET Core apps. That means you can reuse a lot of the same libraries and classes as you do already (with a big caveat, which I'll come to later).

You can also use a similar pattern for configuring an application, though there's no CreateDefaultBuilder as of yet, so you need to use the ConfigureServices extension methods etc. For example, a basic hosted service might look something like the following:

public class Program
{
    public static int Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder BuildHost(string[] args) =>
        new HostBuilder()
            .ConfigureLogging(logBuilder => logBuilder.AddConsole())
            .ConfigureHostConfiguration(builder => // setup your app's configuration
            {
                builder
                    .SetBasePath(Directory.GetCurrentDirectory())
                    .AddJsonFile("appsettings.json")
                    .AddEnvironmentVariables();
            })
            .ConfigureServices( services => // configure DI, including the actual background services
                services.AddSingleton<IHostedService, PrintTimeService>());
}

There are a number of differences visible in this program.cs file, compared to a standard ASP.NET Core app:

  1. No default builder - As there's no default builder, you'll need to explicitly call each of the logging/configuration etc extension methods. It makes samples more verbose, but in reality I find this to be the common approach for apps of any size anyway.
  2. No Kestrel - Kestrel is the HTTP server, so we don't (and you can't) use it for generic hosted services.
  3. You can't use a Startup class - Notice that I called ConfigureServices directly on the HostBuilder instance. This is possible in a standard ASP.NET Core app, but it's more common to configure your services in a separate Startup class, along with your middleware pipeline. Generic hosted services don't have that capability. Personally, I find that a little frustrating, and would like to see that feature make it's way to HostBuilder.

There's actually one other major difference which isn't visible from these samples. The IHostBuilder abstraction and it's associates are in a completely different namespace and package to the existing IWebHostBuilder. This causes a lot of compatibility headaches, as you'll see.

Same interfaces, different namespaces

When I started using the generic host, I had made a specific (incorrect) assumption about how the IHostBuilder and IWebHostBuilder were related. Given that they provided very similar cross-cutting functionality to an app (configuration, logging, DI), I assumed that they shared a common base interface. Specifically, I assumed the IWebHostBuilder would be derived from the IHostBuilder - it provides the same functionality and adds HTTP on top, so that seemed logical to me. However, the two interfaces are completely unrelated!

The ASP.NET Core HTTP hosting abstractions

The ASP.NET Core hosting abstractions library, which contains the definition of IWebHostBuilder, is Microsoft.AspNetCore.Hosting.Abstractions. This library contains all the basic classes and interfaces for building an ASP.NET Core web host, e.g.

  • IWebHostBuilder
  • IWebHost
  • IHostingEnvironment
  • WebHostBuilderContext

These interfaces all live in the Microsoft.AspNetCore.Hosting namespace. As an example, here's the IWebHostBuilder interface:

public interface IWebHostBuilder
{
    IWebHost Build();
    IWebHostBuilder ConfigureAppConfiguration(Action<WebHostBuilderContext, IConfigurationBuilder> configureDelegate);
    IWebHostBuilder ConfigureServices(Action<IServiceCollection> configureServices);
    IWebHostBuilder ConfigureServices(Action<WebHostBuilderContext, IServiceCollection> configureServices);
    string GetSetting(string key);
    IWebHostBuilder UseSetting(string key, string value);
}

The ASP.NET Core generic host abstractions

The generic host abstractions can be found in the Microsoft.Extensions.Hosting.Abstractions library (Extensions instead of AspNetCore). This library contains equivalents of most of the abstractions found in the HTTP hosting abstractions library:

  • IHostBuilder
  • IHost
  • IHostingEnvironment
  • HostBuilderContext

These interfaces all live in the Microsoft.Extensions.Hosting namespace (again, Extensions instead of AspNetCore). The IHostBuilder interface looks like this:

public interface IHostBuilder
{
    IDictionary<object, object> Properties { get; }
    IHost Build();
    IHostBuilder ConfigureAppConfiguration(Action<HostBuilderContext, IConfigurationBuilder> configureDelegate);
    IHostBuilder ConfigureContainer<TContainerBuilder>(Action<HostBuilderContext, TContainerBuilder> configureDelegate);
    IHostBuilder ConfigureHostConfiguration(Action<IConfigurationBuilder> configureDelegate);
    IHostBuilder ConfigureServices(Action<HostBuilderContext, IServiceCollection> configureDelegate);
    IHostBuilder UseServiceProviderFactory<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder> factory);
}

If you compare this interface to the IWebHostBuilder I showed previously, you'll see some similarities, and some differences. On the similarity side:

  • Both interfaces have a Build() function that returns their respective "Host" interface.
  • Both have a ConfigureAppConfiguration method for setting the app configuration. While both interfaces use the same Microsoft.Extensions.Configuration abstraction IConfigurationBuilder, they each use a different context object - HostBuilderContext or WebHostBuilderContext.
  • Both have a ConfigureServices method, though again the type of the context object differs.

There are many more differences between the interfaces. To highlight a few:

  • The IHostBuilder has a ConfigureHostConfiguration method, for setting host configuration rather than app configuration. This is equivalent to the UseConfiguration extension method on IWebHostBuilder (which under the hood calls IWebHostBuilder.UseSetting).
  • The IHostBuilder has explicit methods for configuring the DI container. This is normally handled in the Startup class for IWebHostBuilder. As HostBuilder doesn't use Startup classes, the functionality is exposed here instead.

These changes, and the lack of a common interface, are just enough to make it difficult to move code that was working in a standard ASP.NET Core app to a generic host app. Which is really annoying!

So why all the changes? To be honest, I haven't dug through GitHub issues and commits to find out, but I'm happy to speculate.

It's always about backward compatibility

The easiest way to avoid breaking something, is to not change it! My guess is that's why we're stuck with these two similar-yet-irritatingly-different interfaces. If Microsoft were to introduce a new common interface, they'd have to modify IWebHostBuilder to implement that interface:

public interface IHostBuilderBase
{
    IWebHost Build();
}

public interface IWebHostBuilder: IHostBuilderBase
{
    // IWebHost Build();         <- moved up to base interface
    IWebHostBuilder ConfigureAppConfiguration(Action<WebHostBuilderContext, IConfigurationBuilder> configureDelegate);
    IWebHostBuilder ConfigureServices(Action<IServiceCollection> configureServices);
    IWebHostBuilder ConfigureServices(Action<WebHostBuilderContext, IServiceCollection> configureServices);
    string GetSetting(string key);
    IWebHostBuilder UseSetting(string key, string value);
}

On first look that might seem fine - as long as they only moved methods from IWebHostBuilder to the base interface, and made sure the signatures were the same, any classes implementing IWebHostBuilder would still correctly implement it. But what if the interface was implemented explicitly? For example:

public class MyWebHostBuilder : IWebHostBuilder
{
    IWebHost IWebHostBuilder.Build() // explicitly implement the interface
    {
        // implementation
    }
    // other methods
}

I'm not 100%, but I suspect that would break some things like overload resolution and such, so would be a no-go for a minor release (and likely a major release to be honest).

The other advantage of creating a completely separate set of abstractions, is a clean slate! For example, the addition of the ConfigureHostConfiguration() method to IHostBuilder suggests an acknowledgment that it should have been a first class citizen for the IWebHostBuilder as well. It also leaves the abstractions free to evolve in their own way.

So if creating a new set of abstractions libraries gives us all these advantages, what's the downside, what do we lose?

Code reuse is out the window

The big problem with the approach of creating new abstractions, is that we have new abstractions! Any "reusable" code that was written for use with the Microsoft.AspNetCore.Hosting abstractions, has to be duplicated if you want to use it with the Microsoft.Extensions.Hosting.

Here's a simple example of the problem that I ran into almost immediately. Imagine you're written an extension method on IHostingEnvironment to check if the current environment is Testing:

using System;
using Microsoft.AspNetCore.Hosting;

public static class HostingEnvironmentExtensions
{
    const string Testing = "Testing";
    public static bool IsTesting(this IHostingEnvironment env)
    {
        return string.Equals(env.EnvironmentName, Testing, StringComparison.OrdinalIgnoreCase);
    }
}

It's a simple method, you might use it in various places in your app, in the same way the built-in IsProduction() and IsDevelopment() extension methods are.

Unfortunately, this extension method can't be used in generic hosted services. The IHostingEnvironment used by this code is a different IHostingEnvironment to the generic host abstraction (Extensions namespace vs. AspNetCore namespace).

That means if you have common library code you wanted to share between your HTTP and non-HTTP ASP.NET Core apps, you can't use any of the abstractions found in the hosting abstraction libraries. If you _do_ need to use them, you're left copy-pasting code ☹.

Another example of the issue I found is for third-party libraries that are used for configuration, logging, or DI, and that have a dependency on the hosting abstractions.

For example, I commonly use the excellent Serilog library to add logging to my ASP.NET Core apps. The Serilog.AspNetCore library makes it very easy to add an existing Serilog configuration to your app, with a call to UseSerilog() when configuring your WebHostBuilder:

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>()
            .UseSerilog() // <-- Add this line

Unfortunately, even though the underlying configuration libraries are identical between IWebHostBuilder and IHostBuilder, the UseSerilog() extension method is not available. It's an extension method on IWebHostBuilder not IHostBuilder, which means you can't use the Serilog.AspNetCore library with the generic host.

To get round the issue, I've created a similar library for adding Serilog to generic hosts, called Serilog.Hosting.Extensions that you can find on GitHub. Thanks to everyone in the Serilog project for adopting it officially into the fold, and for making the whole process painless and enjoyable! In my next post I'll cover how to use the library in your generic ASP.NET Core apps.

These problems will basically apply to all code written that depends on the hosting abstractions. The only real way around them is to duplicate the code, and tweak some names and namespaces. It all feels like a missed opportunity to create something cleaner, with an easy upgrade path, and is asking for maintainability issues. As I discussed in the previous section, I'm sure the team have their reasons for the approach taken, but for me, it stings a bit.

Summary

In this post I discussed some of the similarities and differences between the hosting abstractions used in the HTTP ASP.NET Core apps and the non-HTTP generic host. Many of the APIs are similar, but the main hosting abstractions exist in different libraries and namespaces, and aren't iteroperable. That means that code written for one set of abstractions can't be used with the other. Unfortunately, that means there's likely going to be duplicate code required if you want to share behaviour between HTTP and non-HTTP apps.