I wrote a while back on getting your Windows Services building with .Net Core which is a nice way to use the new SDK pipeline but still host on Windows without much fuss.

But what If you like the approach but want to run on Linux?

Well after some stumbling around in the dotnet core docs, I found information about HostBuilder, which looks a lot like the WebHostBuilder and WebHost that we use with AspNetCore. And that is no accident.

This new set of classes is a new feature from dotnet core 2.1:

The Generic Host is new in ASP.NET Core 2.1 and isn't suitable for web hosting scenarios. For web hosting scenarios, use the Web Host. The Generic Host is under development to replace the Web Host in a future release and act as the primary host API in both HTTP and non-HTTP scenarios. - microsoft docs

If you are used to the WebHostBuilder (WebHost.CreateWebHostBuilder(args).Build().Run();) then you will find the contracts for this very similar. Let's look at an example of what a console app using this new library might look like.

Like last time, we will use this simple application that will sleep for 1 second, then try to generate the next number in the Fibonacci Sequence and print it out.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace MyApp
{
    public class MyApp
    {
        private static readonly AutoResetEvent _closeRequested = new AutoResetEvent(false);
        private long _last = 0;
        private long _current = 0;
        private Task _work;

        public MyApp()
        {
        }

        public void Start()
        {
            _work = Task.Run(() => DoWorkLoop());
        }

        public void Stop()
        {
            _closeRequested.Set();
            if (_work != null)
            {
                _work.Wait();
                _work = null;
            }
        }

        public void DoWorkLoop()
        {
            while (!_closeRequested.WaitOne(1000))
            {
                var last = _current;
                var next = _last + _current;
                if (next == 0)
                {
                    next = 1;
                }
                _last = _current;
                _current = next;
                Console.WriteLine(next);
            }
        }
    }
}

So how can we use this hosted in the new HostBuilder?

First, we have to use the new IHostedService interface:

public interface IHostedService
{
    // Called when the host is ready to start the service
    Task StartAsync(CancellationToken cancellationToken);

    // Called on graceful shutdown by the host
    Task StopAsync(CancellationToken cancellationToken);
}

Notice the interface uses CancellationToken. This is called when the host needs to cancel the action (such as termination of the application during startup, or if you take too long during shutdown).

Our app class can be modified to meet these requirements:

public class MyApp : IHostedService
{
    private static readonly AutoResetEvent _closeRequested = new AutoResetEvent(false);
    ...
    private Task _work;

    Task StartAsync(CancellationToken cancellationToken)
    {
        _work = Task.Run(() => DoWorkLoop());
        return Task.CompletedTask;
    }

    Task StopAsync(CancellationToken cancellationToken)
    {
        _closeRequested.Set();
        if (_work != null)
        {
            _task.Wait(cancellationToken);
            _work.Dispose();
            _work = null;
        }

        return Task.CompletedTask;
    }

    public void DoWorkLoop()
    {
        ...
    }
}

Now we meet the expected interface, we can build our startup code like so:

public static class Program
{
    public static async Task Main(string[] args)
    {
        await new HostBuilder()
            .UseHostedService<MyApp>()
            .RunConsoleAsync();
    }
}

This is basically the minimum to get this working. But there is so much more you can do now you have a host builder.

Let's break this down into what those extensions are actually doing for you. The above is essentially equivalent to this:

var host = new HostBuilder()
    .ConfigureServices(services =>
        services.AddHostedService<MyApp>())
    .UseConsoleLifetime()
    .Build();

using (host)
{
    await host.StartAsync();

    await host.WaitForShutdownAsync();
}

UseConsoleLifetime, which is used by RunConsoleAsync() internally, waits for Ctrl+C/SIGINT or SIGTERM commands from the OS (windows mac and Linux where applicable) to make the application compatible with graceful shutdown scenarios.

RunConsoleAsync() also bootstraps the Start and WaitForShutdown usage, making it a really handy helper function to know and use.

ConfigureServices is identical to that you are familiar with on the WebHostBuilder, and Startup classes. This is your ServiceCollection where you can register dependencies. That's right, this has AspNetCore's Dependency Resolution structure. AddHostedService is just a shorthand to register you class as an implementation of IHostedService that the Generic Host can later resolve to start and stop for you.

While talking about AddHostedService, I should mention you can have as many IHostedService instances registered that you want to have running. Multiple worker processes hosted in one app essentially.

Let's talk about configuration.

AspNetCore introduced the IConfiguration and the appsettings.json files. I've found that adding the following code gives you a very similar experience:

new HostBuilder()
   .UseEnvironment(Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"))
    .ConfigureAppConfiguration((hostContext, configApp) =>
    {
        // ConfigureAppConfiguration makes sure the config is available
        // as runtime in the IConfiguration.
        // You can use ConfigureHostConfiguration which configures the
        // IHostingEnvironment's Build-type Configuration instead if you
        // care to make the distinction.
        // ConfigureAppConfiguration changes are also still visible on
        // IHostingEnvironment as well ¯\_(ツ)_/¯
        configApp
            .AddJsonFile("appsettings.json", optional: false, reloadOnChange: false)
            .AddJsonFile($"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json", optional: true)
            .AddEnvironmentVariables()
            .AddCommandLine(args);
    })
    .ConfigureServices(services =>
    {
        ...
        
        services.AddHostedService<MyApp>();
    }
    .Build();

This configuration should give you the appsettings loading, including the Environment-specific config files using the same ASPNETCORE_ENVIRONMENT EnvironmentVariable you are used to (though you could call this whatever you want, really). It also loads EnvironmentVariables as config overrides, and enables command line argument overrides as well (as highest priority).

I will leave you with a mention of logging.

.ConfigureLogging((hostcontext, loggingBuilder) =>
{
    // I'm thinking you should go do your own reading on this one, I'm not going to give you ALL the code to copy-paste from!
    // https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-2.1#configurelogging
    ...
})

Happy Coding!