Creating Windows Services In .NET Core – Part 1 – The “Microsoft” Way

This article is part of a series on creating Windows Services in .NET Core.

Part 1 – The “Microsoft” Way
Part 2 – The “Topshelf” Way


Creating Windows Services to do batch jobs or in general do background work used to be a pretty common pattern, but you don’t often come across them anymore due to the proliferation of cloud services such as Amazon Lambda, Azure WebJobs or Azure Functions taking their place. Personally, I’m a big fan of using Azure WebJobs these days as it basically means I can write a console application without any thought to running it in the cloud, and then with a single batch file, turn it into an automated job that can run 24/7.

But maybe you’re still on bare metal. Or maybe you have a bunch of legacy apps that are running as Windows Services that you want to convert to .NET Core, but not go the whole way to converting them to “serverless”. This is the tutorial for you.

In many respects, a Windows Service in .NET Core is exactly the same as one in .NET Framework. But there are a few little tricks that you might stumble across along the way. Furthermore, there is the “Microsoft” way of building a Windows Service which we will look at in this article, and 3rd party libraries such as TopShelf that make the process much easier (We will look at Topshelf in Part 2).

Setup

There is no Visual Studio “template” for creating a Windows Service, so instead create a regular .NET Core console application project.

Once created, we need to install a nuget package that adds in a bunch of Windows specific API’s into .NET Core. These are API’s that are actually already available in full framework, but many of them are very specific to Windows e.x. Windows Services. For that reason they aren’t in the .NET Core base, but can be added via a single handy dandy package. (You can read more about the package here : https://devblogs.microsoft.com/dotnet/announcing-the-windows-compatibility-pack-for-net-core/)

Let’s go ahead and run the following from our Package Manager Console :

The Code

What we are most interested in from the above Nuget is the ServiceBase class. This is a base class for writing Windows Services, and provides “hooks” for events involved in the service e.g. Start, Stop, Pause etc.

We are going to create a class in our code that does a simple logging job to a temp file, just so we can see how it works. Our code looks like :

So you’ll notice we are inheriting from the “ServiceBase”, and we are just overriding a couple of events to log. A typical pattern would involve kicking off a background thread “OnStart”, and then aborting that thread “OnStop”. Heavy lifting should not be happening inside the OnStart event at all!

From our Main entry method, we want to kick off this service. It’s actually pretty easy :

That’s it!

Deploying Our Service

When publishing our service, we can’t just rely on a bog standard Visual Studio build to get what we want, we need to be building specifically for the Windows Runtime. To do that, we can run the following from a command prompt in the root of our project. Notice that we pass in the -r flag to tell it which platform to build for.

If we check our /bin/release/netcoreappX.X/publish directory, we will have the output from our publish, but most importantly we will have an exe of our application in here. If we didn’t specify the runtime, we would instead get a .NET Core .dll which we are unable to use as a Windows Service.

We can move this publish directory anywhere to install, but let’s just work directly from the publish folder for now.

For this next part, you will need to open a command prompt as an administrator. Then use the following command :

The SC command is a bog standard windows command (Has nothing to do with .NET Core), that installs a windows service. We say that we are creating a windows service, and we want to call it “TestService”. Importantly, we pass in the full path to our windows service exe.

We should be greeted with :

Then all we need to do is start our service :

We can now check our log file and see our service running!

To stop and delete our service, all we need to do is :

Debugging Our Service

Here is where I really think doing things the Microsoft way falls down. Debugging our service becomes a real chore.

For starters our overridden methods from the ServiceBase are set to be protected. This means that we can’t access them outside our class, which makes debugging them even harder. The best way I found was to have a public method for each event that the protected method also calls. Bit confusing, but works like this :

This atleast allows us to do things like :

This to me is rough as hell involves lots of jiggery pokery to get things going.

Your other option is to do a release in debug mode, actually install the service, and then attach the debugger. This is actually how Microsoft recommends you debug your services, but I think it’s just a hell of a lot of messing about.

What’s Next

There’s actually some other really helpful things we could do here. Like creating an install.bat file that runs our SC Create  command for us, but in my opinion, the debugging issues we’ve seen above are enough for me to immediately tap out. Luckily there is a helpful library called Topshelf that takes a tonne of the pain away, and in the next part of this series we will be looking at how we can get going with that.

ENJOY THIS POST?
Join over 3.000 subscribers who are receiving our weekly post digest, a roundup of this weeks blog posts.
We hate spam. Your email address will not be sold or shared with anyone else.

3 comments

  1. .NET Core 3.0 has a new template for it:
    > dotnet new worker
    Then add a reference to Microsoft.Extensions.Hosting.WindowsServices package.
    Now .UseWindowsService() in program.cs to convert your background service to a Windows NT service.

      1. Plus you can run the worker app as a Windows service or a Linux daemon. I haven’t looked into it too much yet, but it sounds nice. I’ll look forward to your Part 3. 🙂

Leave a Reply

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