Get started with .NET Generic Host

Learn how Microsoft made it easier to bootstrap your new .NET Core project and get started with .NET Generic host.

In the version bump to .NET Core 2.1, Microsoft added the .NET Generic Host, which is a non-web version of the WebHost that runs ASP.NET Core. The thought behind this addition was to allow us to re-use the tools that we use in ASP.NET, such as dependency injection and logging abstractions of Microsoft.Extensions.

Later on, in ASP.NET Core 3.0 and 3.1, they moved ASP.NET to run on .NET Generic Host instead of the previously used WebHost, merging the two approaches together to make them truly the same. Now we can build our console applications, systemd’s, windows services or web apps on the same underlying hosting paradigm, with the same shared abstractions.

The motivation to move to a generic host approach was partly sparked by the recent popularity of gRPC and the desire to add additional stack on of the generic host, which led to the addition of gRPC features in ASP.NET Core 3.0. Heres why you should use gRPC for everything.

The basics

Starting a new application of any sort, require you at some point to handle these issues:

  • Dependency Injection(DI)
  • Logging
  • Configuration
  • Lifetime management

All of these issues, are pretty much solved and given to us if we start the application off building on the .NET Generic Host HostBuilder.

The .NET Generic host is built on top of the original abstractions for these tools from ASP.NET, so if you have worked with ASP.NET Core before, you will feel right at home, as .NET Generic Host gets you up and running with all these tools, right out of the box.

Setting up a Host

Setting up a Host usually happens in the Program.cs file, in the main method. It simply bootstraps your application to be able to run.

We use the CreateDefaultBuilder() to setup a Host with default settings such as:

  • Setting the application content root.
  • Loading host configuration from Environment variables, and command-line arguments.
  • Sets up appsettings.json and appsettings.{Environment}.json as default locations for our IConfiguration.
  • Adds the Secret Manager to IConfiguration() when we’re running in Development mode.
  • Adds the configuration to IConfiguration() from Environment variables, and command-line arguments.
  • Adds default logging providers: Console, Debug, EventSource, and EventLog(Windows only).
  • Enables scope validation and dependency validation when the environment is Development.
  • Initiates the Dependency Injection system.
  • Selects default IHostLifeTime. Which handles notifying parties about the application start, and later shut down.
    • The default is: Microsoft.Extensions.Hosting.Internal.ConsoleLifetime which listens for Ctrl+C/SIGINT or SIGTERM and calls StopApplication to start the shutdown process.
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
...

class Program
{
    static async Task Main(string[] args)
    {
        await CreateHostBuilder(args).Build().RunAsync();
    }

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

Our Application is registered as a Hosted Service. This means that we register a Service class, that implements IHostedService, we have written that can receive StartAsync and StopAsync events from the Hosting Environment. We can register as many of these as we want, but generally, you would only register once for each application you have.

.ConfigureServices((hostContext, services) =>
{
    services.AddHostedService<MyService>();
})

An IHostedService must implement two methods; StartAsync and StopAsync, these communicate to our program that the caller wants the application to start or stop. And as you can see, we are also injecting an ILogger<MyService> which is automatically registered in the DI system.

internal class MyService : IHostedService
{
    private ILogger<MyService> Logger { get; }

    public MyService(ILogger<MyService> logger)
    {
        Logger = logger;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }
}

We can expand our service with a little more code, to get to a proper “Hello World stage”.

internal class MyService : IHostedService
{
    private ILogger<MyService> Logger { get; }
    private CancellationTokenSource CancellationTokenSource { get; } = new CancellationTokenSource();
    private TaskCompletionSource<bool> TaskCompletionSource { get; } = new TaskCompletionSource<bool>();

    public MyService(ILogger<MyService> logger)
    {
        Logger = logger;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        // Start our application code.
        Task.Run(() => DoWork(CancellationTokenSource.Token));
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        CancellationTokenSource.Cancel();
        // Defer completion promise, until our application has reported it is done.
        return TaskCompletionSource.Task;
    }

    public async Task DoWork(CancellationToken cancellationToken)
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            Logger.LogInformation("Hello World");
            await Task.Delay(1000);
        }
        Logger.LogInformation("Stopping");
        TaskCompletionSource.SetResult(true);
    }

}

The above application gives the following Console output:

.NET Generic Host Hello World
.NET Generic Host Hello World

We basically have an application running now, that we can build into a fully-fledged windows service, or stay as a console application.

Dependency Injection

One of the great parts of the .NET Generic Host, is that it lets you get bootstrap with Dependency Injection immediately. As we use the CreateDefaultBuilder() everything is basically hooked up to start running, but we can start adding our own dependencies.

private static IHostBuilder CreateHostBuilder(string[] args)
{
    return Host.CreateDefaultBuilder()
        .ConfigureServices((hostContext, services) =>
        {
            // Register our hosted service / program
            services.AddHostedService<MyService>();
            // Register our services
            services.AddTransient<WorkerFactory>();
            services.AddTransient<Worker>();
            services.AddScoped<ScopedService>();
            services.AddSingleton<SingletonsAreBadDotCom>();
        });
}

Added dependencies, is as easy as you know it from the Startup.cs class in ASP.NET Core.

Configuration

If you want to use configuration for your application, and the default settings are not enough, you can configure that yourself as well.

Host.CreateDefaultBuilder(args)
    .ConfigureServices((hostContext, services) => {} // Collapsed
    .ConfigureHostConfiguration(configHost =>
    {
        configHost.SetBasePath(Directory.GetCurrentDirectory());
        configHost.AddJsonFile("mysettings.json", optional: true);
        configHost.AddEnvironmentVariables(prefix: "MYCOMPANY_");
        configHost.AddCommandLine(args);
    });

Application lifetime management

The IHostLifetime interface is what connects the underlying platform, to the .NET Generic Host lifetime events.

The default IHostLifeTime is the ConsoleLifetime, which also is what the ASP.NET Core implementations are using.

public interface IHostLifetime
{
    Task WaitForStartAsync(CancellationToken cancellationToken);
    Task StopAsync(CancellationToken cancellationToken);
}

Currently, the following pre-made IHostingLifetime implementations are available:

  • ConsoleLifetime [Default] – Listens for Ctrl+C/SIGINT or SIGTERM and stops the application.
  • SystemdLifetime – Listens for SIGTERM and stops the host application, and notifies systemd about state changes (Ready and Stopping)
  • WindowsServiceLifetime – Hooks into the Windows Service events for lifetime management

Lifetime management – Application Services

If you have services in your application, that needs to know that the underlying hosting environment has started successfully, is shutting down or is finally stopped. You can simply inject IHostApplicationLifetime, and be notified as these events occur:

internal class MyService : IHostedService
{
    private ILogger<MyService> Logger { get; }
    private IHostApplicationLifetime AppLifetime  { get; };

    public MyService(ILogger<MyService> logger, IHostApplicationLifetime appLifetime)
    {
        Logger = logger;
        AppLifetime = appLifetime;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        AppLifetime.ApplicationStarted.Register(OnStarted);
        AppLifetime.ApplicationStopping.Register(OnStopping);
        AppLifetime.ApplicationStopped.Register(OnStopped);

        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }
    
    private void OnStarted()
    {
        _logger.LogInformation("OnStarted has been called.");
        // Handle OnStarted
    }

    private void OnStopping()
    {
        _logger.LogInformation("OnStopping called.");
        // Handle OnStopping
    }

    private void OnStopped()
    {
        _logger.LogInformation("OnStopped called.");
        // Handle OnStopped
    }
}

FAQ

Q: Can this handle console applications that start up, do some work, and then end?

Yes! but you need to tell it that you are done.

You need to inject IApplicationLifetime, where you can call applicationLifetime.StopApplication(); when you are done handling the task.

This will then trigger the shutdown procedures.

I hope this article was helpful to you, and you see the benefits in using .NET Generic Host.

// André Kock

References:

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *