Running a .Net Core console application as a Windows Service

September 2019 update: This subject has been an ever-moving feast since this article was first published in the early days of .Net Core, but there are now four main approaches to this that are each discussed below.

Building an application to run as a Windows Service was pretty straightforward with the .Net Framework. You either implemented ServiceBase in a console application or used a helper library like TopShelf. Both approaches provided a programming model that covered the full life cycle of the service, from initial registration to running, pausing and stopping.

When .Net Core was first released it didn’t have any direct support for creating Windows Services in .Net Core. The ServiceBase class was left out of the initial implementation, presumably because it contained too much Windows implementation detail for what was supposed to be a cross-platform framework.

Since then several different approaches to creating a service have emerged:

  • Using a service manager like NSSM to manage the registration and running of the service.
  • Building a native service using the Windows Compatibility Pack for .Net Core
  • Creating a service using Topshelf
  • Using .Net Core’s generic host builder to run background services as a Windows Service

Using a service manager

The most direct way of creating a Windows Service in .Net Core is to create a simple console application and use an external service manager like NSSM to take care of the service aspects. You don’t even have to compile your .Net Core console application for Windows as NSSM can be configured to call a batch file.

To install a .Net Core application as a windows service just create a batch file called run.bat in your application root that contains the following command:

dotnet run

You can register this batch file as a windows service called “testservice” by dropping NSSM.EXE into your application directory and running the following:

nssm install testservice

This will display the installation dialog where you enter the full path to your run.bat file in the Path field.

The catch with using an external service manager is there is no direct support for handling aspects of the service lifecycle beyond registering and starting the service. Most importantly, there’s no equivalent of the OnStop event in ServiceBase that allows you to clean up resources gracefully when a service is stopped. There isn’t even a reliable event that you can hook into when the process ends or console window closes.

With an NSSM-based service the only way to hook into a service stop is to intercept the CTRL+C command – this is available in .Net Core using the Console object as shown below:

Console.CancelKeyPress += Console_CancelKeyPress;

private static void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e)
{
    // Put your clean-up code here
}

When you register a service using NSSM make sure that it is configured to issue a CTRL+C command when shutting down the service. This is done by default but you can check it by looking at the ShutDown tab on the service registry dialog:

Note that although this allows you to clean up when a service is stopped, this approach can only support a rudimentary service that can be switched on and off. There’s still no support for other aspects of the service lifecycle including pausing, continuing and responding to power events.

Building a native service

The Windows Compatibility Pack contains the ServiceBase class, allowing you to build Windows Services in much the same way that you would with the .Net Framework.

Whether you’d want to take this approach is a different question altogether. The Windows Compatibility Pack was designed to support porting legacy .Net Framework code rather than acting as a foundation for new applications. This style of Windows Service project can also be awkward to debug, requiring you to manually attach a debugger to the process that the service is running on.

The code is simple enough. The example below shows a basic service class that overrides the main methods to start, stop and pause the service:

public class NativeServiceImpl : ServiceBase
{
    protected override void OnStart(string[] args)
    {
        // The service has been started
        base.OnStart(args);
    }
 
    protected override void OnStop()
    {
        // The service has been stopped
        base.OnStop();
    }
 
    protected override void OnPause()
    {
        // The service has been paused
        base.OnPause();
    }
}

The service can be set up in the application’s Program.Main() method:

static void Main(string[] args)
{
    ServiceBase.Run(new NativeServiceImpl());
}

You’ll need to build the application for the Windows runtime using the -r flag:

dotnet publish -r win-x64 -c Release

The service can then be installed as normal using either sc.exe or installutil.exe.

Using Topshelf

Topshelf gained a lot of traction in the .Net Framework community as it simplified the task of building, configuring and debugging Windows Services. It was slow to offer a version that was compatible with .Net Core, though a .Net Standard version was finally released in September 2018.

A Topshelf implementation involves implementing an abstract ServiceControl class:

public class TopShelfServiceImpl : ServiceControl
{
    public bool Start(HostControl hostControl)
    {
        return true;
    }
 
    public bool Stop(HostControl hostControl)
    {
        return true;
    }
}

The service itself is run using the HostFactory class in the application’s Program.Main() method:

static void Main(string[] args)
{
    HostFactory.Run(x => x.Service<TopShelfServiceImpl>());
}

The application needs to be built for the windows runtime in much the same way as a native service. This may seem much like building and deploying a native service, but the experience of installing and debugging a Topshelf service is very different – in a good way.

You can install and start by running the application directly with command line arguments, e.g.

TopShelfServer.exe install
TopShelfServer.exe start

There are a wide range of supported command line arguments and you’ll never need an external registration utility. For debugging Topshelf emulates starting the service in a console window, running the service in the background and automatically attaching the debugger. This means you can start a service from within the Visual Studio IDE and have it hit breakpoints much like any other application.

Using the .Net Core generic host to run background services

The generic host builder that was added in .Net Core 3.0 can also be used to build Windows Services. This takes care of the application lifecycle for you, including controlling startup and graceful shutdown, logging, dependency injection and configuration. You can also get it to run as a Windows Service.

With this approach, you create a service definition based on the BackgroundService base class. This class can also be used to execute long-running tasks in ASP.Net applications and it has a single method override as shown below:

public class BackgroundServiceImpl : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // Do some work
    }
}

The service definition is wired up to the host builder in Program.Main:

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

To turn this into a Windows Service, you need to add the Microsoft.Extensions.Hosting.WindowsServices package which lets you add the UseWindowsService() extension method to the host builder:

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

This gives you a vanilla .Net console app that can be debugged directly from Visual Studio. From here you need to build the project for the windows ruintime and install the application using sx.exe or installutil.exe.

This approach feels like a snug fit to the vanilla .Net Core ecosystem. It provides a flexible style of implementation that you can easily run in other contexts. The application can easily be run as a containerised service and the background services can also be run in an ASP.Net application. That said, it does lack the bells and whistles that Topshelf gives you, particularly when it comes to installing and configuring a service.