.Net Core and above (5,6,...) windows service / linux systemd

Photo by Alex Chumak on Unsplash

.Net Core and above (5,6,...) windows service / linux systemd

Intro

While windows services had their own implementation classes in the .Net Framework, when migrating to .Net (Core) we noticed that we had to re-implement the whole service control.

Our requirements are, imho, basic requirements for a background windows service:

  • Is getting notified when the service controller is stopping the service
  • Can stop itself from within on a fatal error
  • Will run below windows service and linux systemd

Required nuget

For integration we require the NuGet package Microsoft.Extensions.Hosting.WindowsServices. There is also one for the systemd part of Linux (Microsoft.Extensions.Hosting.Systemd), however I will cover that later.

The main entry point for the service

We use the Program.cs for starting up the service. Normally, before I start the service itself, I verify the settings and so on. If we just return from the main method, the service will not start using the windows service controller and throw an error 'service could not be started'.

static async Task Main(string[] args)
{
    // load settings / verify environment....

    // start the service 
    using IHost host = Host.CreateDefaultBuilder(args)
        .UseWindowsService(options =>
            {
                options.ServiceName = "MyServiceName";
            })
            .UseSystemd()
            .ConfigureServices(services =>
            {
                services.AddHostedService<CustomService >();
             })
             .Build();
    await host.RunAsync();
}

Background service class

We start, by deriving our CustomService.cs from BackgroundService where we must override the ExecuteAsync method:

public class CustomService : BackgroundService
{
    protected override Task ExecuteAsync(CancellationToken stoppingToken)
    {
        throw new NotImplementedException();
    }
}

The stoppingToken is cancelled when the service is stopped by the service controller. It is brought to use by the dependency injection, what we can also use to get a reference to the IHostApplicationLifetime from the constructor, which we save us for later in the class itself AND directly register to a delegate which is getting executed when the application is stopping:

    private readonly IHostApplicationLifetime _appLifeTime;

    /// <summary> constructor </summary>
    public CustomService(IHostApplicationLifetime appLifeTime)
    {
        _appLifeTime = appLifeTime;
        _appLifeTime.ApplicationStopping.Register(AppStopping);
    }

    private void AppStopping()
    {

    }

By the delegate registration we do not require to loop, checking the stoppingToken, for the current state.

Stopping ourselves

By using the IHostApplicationLifetime we can stop ourselves pretty simple:

_appLifeTime.StopApplication();

Be aware, that the delegate ApplicationStopping is also executed!

Installation on windows

The previous windows service installation was based on a 'service installer' which was executed after starting the 'installutil.exe' from the .net framework folder

This is not anymore valid, so we use the New-Service cmdlet, which also has got much more options than the previous method using the 'service installer'.

To use the cmdlet, take a look here. I even prefer this method about invoke the windows apis directly.

Installation on linux

On Linux we require a systemd services file. Here we will create a system service called 'sample-dotnet':

vi /etc/systemd/system/sample-dotnet.service

Content:

# reload: systemctl deamon-reload
# enable: systemctl enable itxprdsrv.service

[Unit]
Description=sample dotnet service

[Service]
User=opc
Type=simple
ExecStart=/opt/sample-dotnet
TimeoutStopSec=2

[Install]
WantedBy=multi-user.target

Afterwards, reload and enable systemd:

systemctl deamon-reload
systemctl enable sample-dotnet.service

Naturally you have to set the executable the execute permission:

chmod +x /opt/sample-dotnet

If you have SELinux enabled, this will block it. In development environments we can work around by:

semanage fcontext -a -t bin_t 'sample-dotnet'