Keeping Windows Services Setup Simple with Topshelf and C#


Windows services have been around a long time. Generally, they’re asynchronous programs that run in the background on a Windows machine. Setting up a Windows service can be tedious – that’s where Topshelf comes to the rescue.

From their site (here): “Topshelf is an open source project for hosting services without friction. By referencing Topshelf, your console application *becomes* a service installer with a comprehensive set of command-line options for installing, configuring, and running your application as a service.”

Note: This tutorial is for setting up Topshelf on a Windows machine. The code contains the Linux fix, but the focus will be on Windows. Also, the source code from this post is available on Github here.

You might think, “Why setup services on a Windows machine when you can do it in Linux?”. Well, we have private installs and they must be on Windows machines so are hands are tied. Anyway, onto the code!

First, inside Visual Studio 2022, create a new console application for C# and .NET 6. Name it what you’d like, and use .NET 6.

Let’s start with the worker class. Right-click on the project and select Add->Class and name it MyServiceHandler.cs. Inside the file, add the following test code:

using System.Timers;

namespace TopShelfSample
{
    public class MyServiceHandler
    {
        private System.Timers.Timer aTimer;

        public void Start()
        {
            // startup code here
            // run thread, start timer, or connect to external service (like a queue)
            SetTimer();
        }

        private void SetTimer()
        {
            // Create a timer with a two second interval.
            aTimer = new System.Timers.Timer(2000);
            // Hook up the Elapsed event for the timer. 
            aTimer.Elapsed += OnTimedEvent;
            aTimer.AutoReset = true;
            aTimer.Enabled = true;
        }

        private void OnTimedEvent(Object source, ElapsedEventArgs e)
        {
            Console.WriteLine("The Elapsed event was raised at {0:HH:mm:ss.fff}",
                              e.SignalTime);
        }

        public void Stop()
        {
            // cleanup
            aTimer.Stop();
        }
    }
}

In the start function, this class sets up a Timer that will call ‘OnTimedEvent’ every 2 seconds. The ‘OnTimedEvent’ function just writes to the console the time since last call.

The stop function simply halts the timer.

Next, I’ll add Topshelf to the project. Right-click on the project and select ‘Manage Nuget Packages’. From there, click on the ‘Browse’ tab and type ‘Typeshelf’ into the search input, select the first result and install it:

If you prefer the command line, open the Package Manager Console and type the following:

nuget Install-Package Topshelf

Now, I’ll edit the Program.cs file like so:

using System.Runtime.InteropServices;
using Topshelf;
using Topshelf.Runtime.DotNetCore;
using TopShelfSample;

Console.WriteLine("MyService Starting");


// Main entry
var rc = HostFactory.Run(x =>
{
    // this works if running on Linux
    if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
    {
        x.UseEnvironmentBuilder(new Topshelf.HostConfigurators.EnvironmentBuilderFactory(c => {
            return new DotNetCoreEnvironmentBuilder(c);
        }));
    }

    x.Service<MyServiceHandler>(s =>
    {
        // The system can use dependency injection, if it's setup
        //s.ConstructUsing(name => ServiceProvider.GetService<MyService>());
        s.ConstructUsing(name => new MyServiceHandler());
        // The start function
        s.WhenStarted(tc => tc.Start());
        // The stop function
        s.WhenStopped(tc => tc.Stop());
    });

    // Run as local system user
    x.RunAsLocalSystem();
    // Description that shows up in the services control manager
    x.SetDescription("MyService TopShelf sample");
    // Displayed in services control manager
    x.SetDisplayName("My-Service");
    // Registry name
    x.SetServiceName("MyService");
    // Configure for delayed auto-start 
    x.StartAutomaticallyDelayed();
});

Console.WriteLine("MyService Halted");
var exitCode = (int)Convert.ChangeType(rc, rc.GetTypeCode());
Environment.ExitCode = exitCode;

There’s quite a bit here. I’ll go through it a bit at a time:

First we instantiate the HostFactory.Run static variable and inside that we setup the service.

// this works if running on Linux
    if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
    {
        x.UseEnvironmentBuilder(new Topshelf.HostConfigurators.EnvironmentBuilderFactory(c => {
            return new DotNetCoreEnvironmentBuilder(c);
        }));
    }

If it’s Linux then I use the DotNetCoreEnvironmentBuilder, else it uses the WindowsEnvironmentBuilder.

x.Service<MyServiceHandler>(s =>
    {
        // The system can use dependency injection, if it's setup
        //s.ConstructUsing(name => ServiceProvider.GetService<MyService>());
        s.ConstructUsing(name => new MyServiceHandler());
        // The start function
        s.WhenStarted(tc => tc.Start());
        // The stop function
        s.WhenStopped(tc => tc.Stop());
    });

Here, I set the service itself. So when it starts up, it creates a new instance of MyServiceHandler, and it calls the Start function on start. When stopped, it calls the Stop function.

    // Run as local system user
    x.RunAsLocalSystem();
    // Description that shows up in the services control manager
    x.SetDescription("MyService TopShelf sample");
    // Displayed in services control manager
    x.SetDisplayName("My-Service");
    // Registry name
    x.SetServiceName("MyService");
    // Configure for delayed auto-start 
    x.StartAutomaticallyDelayed();

Here, I set a couple of variables to describe the name and functionality of the service. “x.RunAsLocalSystem()” sets the service user to be the system user. “x.SetDescription” sets the description for the services control manager. “x.SetDisplayName” sets the name in the services control manager. “x.SetServiceName” sets the registered name in the services control manager. Finally, “x.StartAutomaticallyDelayed()” will it to auto-start after a few seconds.

Console.WriteLine("MyService Halted");
var exitCode = (int)Convert.ChangeType(rc, rc.GetTypeCode());
Environment.ExitCode = exitCode;

Outside of the HostFactory, I retrieve the result code (rc) and set it as the exit code.

If you debug the application via F5 in Visual Studio, or “dotnet run” from the command line, you’ll see that the app executes and the output of the OnTimedEvent function is displayed every 2 seconds.

Ok, the program is functional. Now, let’s look at what Topshelf does for the user. First, if you execute the program from the command line or Powershell then it runs like a console application.

Next, to install the application, you simply open an admin command line or Powershell and type “<program name> install”

It’ll show up in the services control manager just like any other service.

To start the service from the command line, type in “<program name> run”

Finally, if the service needs to be uninstalled, simply type in “<program name> uninstall”. Note, these commands must still be run from an admin command line or Powershell.

There are more features that I’m not going to touch on. If you’re interested, check out Topshelf’s site here.

That will conclude this blog post. Thanks for reading!

,