Understanding .NET Generic Host Model
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.
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.
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.
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;
- In Program.cs, configure a new webhost builder object in ConfigureWebHostDefaults
- Register Startup class
GenericHostBuilderExtensions.ConfigureWebHostDefaults
method gets calledGenericHostWebHostBuilderExtensions.ConfigureWebHost
gets called- 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
- https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio
- https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-3.1
- https://andrewlock.net/exploring-the-new-project-file-program-and-the-generic-host-in-asp-net-core-3/