While we are all wanting to get onto the new hotness that is .Net Core cross platform applications, the reality is that our servers are running windows, and we use a combination of Scheduled tasks, IIS, and Windows Services to host our applications. Most .Net Core applications are console apps that work well for Scheduled tasks, and there are tonnes of tutorials for hosting in IIS. I’ve got for you here the how-to for Windows Services.
Introducing TopShelf
If you are writing Windows Services and not using TopShelf then you are doing it wrong (IMHO). Since a colleague introduced me to it 2 years ago it has become the only thing I use for Windows Services.
TopShelf is not a netstandard
target, with the current latest version (4.0.3) instead targeting .Net 4.5.2. However that is fine since we need a few things from the full framework to host as a Windows Service, anyway. (Don’t quote me on that, but for the purposes of using TopShelf today, it will be fine.)
A Simple Service
Let’s look at a simple Windows Service that simply runs a sleep loop and prints to the console. Because lets face it, your service is probably a spin loop of some kind.
public class Processor
{
private bool _running;
private Task _myLoop;
public async Task Start()
{
//Probably do some async data loading here
await Task.Delay(1);
_running = true;
_myLoop = Loop();
}
public async Task Stop()
{
_running = false;
await _myLoop;
}
public async Task Loop()
{
while(_running)
{
await Task.Delay(5000);
System.Console.WriteLine("Everything is fine.");
}
}
}
Hopefully this looks somewhat familiar for a service that can be started and stopped to host as a Windows Service.
Setup the application
First things first, We need make this a .Net 4.5.2 project and include the TopShelf
NuGet package.
{
"version": "1.0.0-*",
"buildOptions": {
"debugType": "portable",
"emitEntryPoint": true
},
"dependencies": {},
"frameworks": {
"net452": {
"dependencies": {
"TopShelf": "4.0.3"
}
}
}
}
Don’t forget to dotnet restore
To host our Processor
, we use TopShelf like this:
public class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
public static void Main()
{
HostFactory.Run(x =>
{
x.Service<Processor>(s =>
{
s.ConstructUsing(() => Create());
s.WhenStarted(p=> p.Start().Wait());
s.WhenStopped(p => p.Stop().Wait());
});
x.RunAsLocalSystem();
x.StartAutomatically();
x.EnableServiceRecovery(r =>
{
r.RestartService(0);
r.RestartService(1);
r.RestartService(2);
r.OnCrashOnly();
//number of days until the error count resets
r.SetResetPeriod(1);
});
x.SetDescription("My App");
x.SetDisplayName("MyApp");
x.SetServiceName("MyApp");
});
}
public static Processor Create()
{
//Allow parameterised configuration here.
return new Processor();
}
}
The nice thing about TopShelf is that you can basically run the application as a console application when it isn’t installed, and It runs basically the same loop:
dotnet build
dotnet run
Control+C
is used to stop the running application. (In case you were wondering, or stuck. Though it does clearly say The MyApp service is now running, press Control+C to exit.
in the output.)
Project MyApp (.NETFramework,Version=v4.5.2) was previously compiled. Skipping compilation.
Configuration Result:
[Success] Name MyApp
[Success] Description My App
[Success] ServiceName MyApp
Topshelf v4.0.0.0, .NET Framework v4.0.30319.42000
The MyApp service is now running, press Control+C to exit.
Everything is fine.
Everything is fine.
Everything is fine.
Control+C detected, attempting to stop service.
The application build output can be seen below. Take not that as this is a .Net 4.5.2 application, we have a nice little exe file as the output.
MyApp
|-- Processor.cs
|-- Program.cs
|-- project.json
|-- project.lock.json
+-- bin
|-- Debug
|-- net452
|-- MyApp.exe
|-- MyApp.pdb
+-- win7-x64
|-- MyApp.exe
|-- MyApp.pdb
|-- Topshelf.dll
Topshelf has a few commands you can use to easily manage it: install
, start
, stop
& uninstall
. There require you to be running as administrator.
dotnet run install
dotnet run start
Alternatively, since it is an exe you can just use:
.\bin\Debug\net452\win7-x64\MyApp.exe install
.\bin\Debug\net452\win7-x64\MyApp.exe start
Take note though, your app cannot be started in console mode if an instance is already installed on the machine.
Package your service
We can easily package our Service using the following command:
dotnet publish -c Release -f net452 -r win7-x64 -o ../MyAppPublished
(Update the -c
(output directory) and -r
(target runtime) as desired.)
This should give us:
MyAppPublished
|-- MyApp.exe
|-- MyApp.pdb
+-- Topshelf.dll
App.Config
If you are migrating onto the dotnet tools, or still have legacy tooling that performs xml transforms on App.config
files then your keen eye would have noticed the lack of a MyApp.exe.config
file above. Not to worry, I have a solution for that, too.
First we add an App.config
file to the project:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
</configuration>
Then we make a few changes to our project.json
file.
{
"version": "1.0.0-*",
"buildOptions": {
"debugType": "portable",
"emitEntryPoint": true
},
"dependencies": {},
"frameworks": {
"net452": {
"buildOptions": {
"copyToOutput": {
"mappings": {
"MyApp.exe.config": "App.config"
}
}
},
"frameworkAssemblies": {
"System.Configuration": "4.0.0.0"
},
"dependencies": {
"TopShelf": "4.0.3"
}
}
}
}
If you run another publish you should now have your config file, too.
dotnet restore
dotnet publish -c Release -f net452 -r win7-x64 -o ../MyAppPublished
Future-proofing
So that’s all well and good to get the old school running through the new tooling. But you might want to use the .Net Core App Runtime as well. Luckily I’ve got an example using a #Define Compiler Directive to share the same codebase across both runtimes.
With .Net core, applications are simply console apps. So you just need an app that runs until it stops (or is killed). At this time, TopShelf doesn’t have a .Net Core version, so we have two different Compiled versions depending on the Compiler Directive. Here is my sample app.
The project.json
:
{
"version": "1.0.0-*",
"buildOptions": {
"debugType": "portable",
"emitEntryPoint": true
},
"dependencies": {},
"frameworks": {
"netcoreapp1.0": {
"dependencies": {
"Microsoft.NETCore.App": {
"type": "platform",
"version": "1.0.0"
}
}
},
"net452": {
"buildOptions": {
"copyToOutput": {
"mappings": {
"MyApp.exe.config": "App.config"
}
},
"define": [
"WindowsService"
]
},
"frameworkAssemblies": {
"System.Configuration": "4.0.0.0"
},
"dependencies": {
"TopShelf": "4.0.3"
}
}
}
}
And a Program.cs
:
using System.Threading.Tasks;
#if WindowsService
using Topshelf;
#endif
namespace MyNewService
{
public class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
public static void Main()
{
#if WindowsService
HostFactory.Run(x =>
{
x.Service<Processor>(s =>
{
s.ConstructUsing(() => Create());
s.WhenStarted(p=> p.Start().Wait());
s.WhenStopped(p => p.Stop().Wait());
});
x.RunAsLocalSystem();
x.StartAutomatically();
x.EnableServiceRecovery(r =>
{
r.RestartService(0);
r.RestartService(1);
r.RestartService(2);
r.OnCrashOnly();
//number of days until the error count resets
r.SetResetPeriod(1);
});
x.SetDescription("My App");
x.SetDisplayName("MyApp");
x.SetServiceName("MyApp");
});
#else
RunAsynchronously().Wait();
#endif
}
private static async Task RunAsynchronously()
{
var processor = Create();
await processor.Start();
System.Console.WriteLine("Press any key to stop program");
System.Console.Read();
await processor.Stop();
}
public static Processor Create()
{
//Allow parameterised configuration here.
return new Processor();
}
}
}
Swap out your own Processor.cs
and you will have a nice Windows Service 4.5.2 app, and a new .Net Core Console app that you can host how you like on Windows and Linux however the new stuff is supposed to be hosted.