Sahan Serasinghe

Senior Software Engineer | Master of Data Science

Understanding .NET Generic Host Model

2020-08-13asp.net core 7 min read

In this article, we will concentrate on how the Generic Host model hosts ASP.NET Core 3.x Web app and a Worker Service. We will first discuss the definition of a Host and its configuration. In the subsequent sections, we will dive into the implementation details from a higher level.

So what’s the deal with the Generic Host

With the separation of execution and initialisation, Generic Host provides us with a cleaner way to configure and start up our apps. By default, when you create an ASP.NET Core app now, your application will be hosted using the Generic Host model. If you create a new worker service app, it will be hosted the same way.

Not only that, but this model also provides you standardised configuration, DI, logging, and many more. You can even create a traditional console app, beef it up and make use of Generic Host.

💡 Follow along with the code from this repository

The Host

According to the official documentation, a Host is,

ASP.NET Core apps configure and launch a host. The host is responsible for app startup and lifetime management. At a minimum, the host configures a server and a request processing pipeline. The host can also set up logging, dependency injection, and configuration.

Let’s create a new .NET 3.1 WebAPI and a Worker Service project

dotnet new webapi -n WebApplication
dotnet new worker -n WorkerService
dotnet new sln
dotnet sln add WebApplication WorkerService

If you open up the solution in an IDE, you will see the following project structure.

dotnet-core-generic-host-1.png

They both have a Program.cs which takes care of setting up a host. In the case of the WebApplication project, it sets up a request processing pipeline defined in a Startup.cs and in the WorkerService project, sets a new hosted service which is an essentially an IHostedService.

In the WebApplication project, when you open up the Program.cs file, you will find the following boilerplate code has been added by the template:

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

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

And, in the WorkerService project we have the following code:

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

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureServices((hostContext, services) =>
        {
            services.AddHostedService<Worker>();
        });

Except for the ConfigureWebHostDefaults() and ConfigureServices(), everything else is the same.

Host Configuration

If you look at the CreateHostBuilder method in the above code, it calls a CreateDefaultBuilder static method from Host coming from Microsoft.Extensions.Hosting namespace. It looks like that when we scaffold an ASP.NET Core app, it gives us a .NET Generic Host by default now. We used to have Web Host in ASP.NET Core 2.x, which was made deprecated since ASP.NET Core 3.0. For any future applications, it is recommended to use the .NET Generic Host.

This does a few things under the covers by wrapping,

  • Dependency Injection services
  • HTTP Server implementation (such as Kestrel)
  • Logging
  • Configuration etc.

In order to get an idea what the above methods do, I looked into the source code on Github.

We will start off with CreateDefaultBuilder method first.

Host.CreateDefaultBuilder()

public static IHostBuilder CreateDefaultBuilder(string[] args)
{
    // Initialize a new HostBuilder object
    var builder = new HostBuilder();

    // Specify the content root directory
    builder.UseContentRoot(Directory.GetCurrentDirectory());

    // Host Configuration : Add environment variables starting with DOTNET_
    // and add any command line args passed
    builder.ConfigureHostConfiguration(config => ... );

    // App Configuration : Add appsettings.json (depending on the env.) files
    // and add user secrets if in development mode
    builder.ConfigureAppConfiguration((hostingContext, config) => ... )

    // Config logging
    .ConfigureLogging((hostingContext, logging) => ... )

    // Use default DI provider
    .UseDefaultServiceProvider((context, options) => ... );

    return builder;
}

As you can see, it pretty much configures a HostBuilder object and returns it. There’s nothing really specific to web hosting in here. This is why it’s common to both HTTP and non-HTTP workloads.

Taking a step further, let’s look at how the web host gets configured. We will now look through ConfigureWebHostDefaults method.

GenericHostBuilderExtensions.ConfigureWebHostDefaults()

public static IHostBuilder ConfigureWebHostDefaults(this IHostBuilder builder, Action<IWebHostBuilder> configure)
{
    return builder.ConfigureWebHost(webHostBuilder =>
    {
        WebHost.ConfigureWebDefaults(webHostBuilder);

        configure(webHostBuilder);
    });
}

Remember that ConfigureWebHostDefaults is used only for HTTP workloads and let’s see what we get as the default web host configuration.

WebHost.ConfigureWebDefaults()

internal static void ConfigureWebDefaults(IWebHostBuilder builder)
{
	// Configure static web assets if in Development mode
    builder.ConfigureAppConfiguration((ctx, cb) => ... );

	// Configure Kestrel
    builder.UseKestrel((builderContext, options) => ... )

	// Configure the default services
    .ConfigureServices((hostingContext, services) => ... )

	// Configure IIS for Windows
    .UseIIS()
    .UseIISIntegration();
}

So far, we have seen that both approaches use the same Generic Host paradigm in the two projects. If you are interested in customising the default configuration, head over to Microsoft Docs’ official documentation.

Finally, how does it all run?

Now comes the interesting part.

In both cases, after the configuration sections, we finally call the Run() on IHost object implemented in HostingAbstractionsHostExtensions. This will run the app and block the calling thread until the host is shut down. This is enabled by WaitForShutdownAsync which is called at the beginning of the start-up process, which can be triggered by Ctrl+C/SIGTERM or SIGINIT.

Let’s look at how both web hosts and worker services run.

For a worker service, remember how we registered our Worker class by passing it into ConfigureServices method. This Worker class extends BackgroundService which in turn implements IHostedService. IHostedService provides 2 methods, namely, StartAsync and StopAsync. So when we run our host, it must be retrieving our Worker service and invoking these methods.

dotnet-core-generic-host-2.png

Source: Microsoft

In the Host.cs there’s a separate StartAsync method and we can find the following lines inside it.

_hostedServices = Services.GetService<IEnumerable<IHostedService>>();

foreach (var hostedService in _hostedServices)
{
    // Fire IHostedService.Start
    await hostedService.StartAsync(combinedCancellationToken).ConfigureAwait(false);
}

So our guess was correct. It certainly invokes the StartAsync method of BackgroundService, that calls ExecuteAsync method in which we have ultimately implemented in our Worker class.

For a web host, there’s a little bit of abstraction on top of this before it hits the above section. A summary of how it reaches this as follows;

  1. In Program.cs, configure a new webhost builder object in ConfigureWebHostDefaults
  2. Register Startup class
  3. GenericHostBuilderExtensions.ConfigureWebHostDefaults method gets called
  4. GenericHostWebHostBuilderExtensions.ConfigureWebHost gets called
  5. Register a GenericWebHostService service
public static IHostBuilder ConfigureWebHost(this IHostBuilder builder, Action<IWebHostBuilder> configure)
{
    var webhostBuilder = new GenericWebHostBuilder(builder);
    configure(webhostBuilder);
    builder.ConfigureServices((context, services) => services.AddHostedService<GenericWebHostService>());
    return builder;
}

So what is a GenericWebHostService ? It’s an IHostedService 🤩. Rest of the story is as above as we looked at in the worker service scenario. Because of this nicely decoupled initialisation we are able to run both ASP.NET Core and Worker services on the Generic Host.

Summary

To summarise, we looked at what makes the Generic Host generic and dug deeper into the implementation details in .NET Github repo. We also looked at what makes an ASP.NET Core web application and a worker service different, configuration-wise. This post became a bit longer than I initially I thought it would be 😅 Nevertheless, hope you picked up a thing or two.

Cheers!

References

  1. https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio
  2. https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-3.1
  3. https://andrewlock.net/exploring-the-new-project-file-program-and-the-generic-host-in-asp-net-core-3/
Loading...
Sahan Serasinghe - Engineering Blog

Sahan Serasinghe Senior Software Engineer at Canva | Azure Solutions Architect Expert | Master of Data Science at UIUC | CKAD