I wrote Building a Windows Service with .Net Core and had a bit of flack because I was only using .Net core to build a .Net 4.5.2 application. Technically the title is still valid, it was a windows service, and I built it using .Net Core tools. But since people came looking for the answer to actually hosting a NetCoreApp application as a Windows Service, I thought it best to follow up with that article as well.
Note that since windows Service logic and hooks are Windows-specific, this solution doesn’t work for Mac or Linux. However I will try to maintain a working Console application, that should satisfy the requirements there.
Just target .Net Full framework
Everything you already have working in your services using System.ServiceProcess.ServiceBase
and other classes from the System.ServiceProcess
full framework assemblies work fine when compiled with .Net Core. Since you can’t run a Windows Service on non-windows platforms, there is no reason not to just target the Windows-only full framework 4.6.2 or 4.7.1 or whichever your stable .Net version of choice is. None of the System.ServiceProcess
code can run on Linux or Mac anyway, and neither can any Windows Service specific code. This is probably going to be the path of least resistance.
But that was the subject of the other article, I assume you are here for something different.
Cross Platform solution
We will now take a look at what we can do to make an application that a) can install and run as a windows service, and b) still runs as a console app on Linux, ignoring the unused service code. This application is going to be a portable netcoreapp2.0
application, so we can only reference netstandard
or netcoreapp
libraries.
dasMulli/dotnet-win32-service is a project that has created a win32 interop layer over the Windows Service API. Much like the way the original .Net code probably works, but compiled as a dotnet standard library (netstandard1.3
and netstandard2.0
compatible versions). On top of this, there is another library PeterKottas/DotNetCore.WindowsService which also targets netstandard2.0
that we will use to give us a nicer install/uninstall interface into our application.
The ‘Application’
Let’s use something pretty dumb. Our app will sleep for 1 second, Then try to generate the next number in the Fibonacci Sequence.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace MyApp
{
public class MyApp
{
private static readonly AutoResetEvent _closeRequested = new AutoResetEvent(false);
private long _last = 0;
private long _current = 0;
private Task _work;
public MyApp()
{
}
public void Start()
{
_work = Task.Run(() => DoWorkLoop());
}
public void Stop()
{
_closeRequested.Set();
if (_work != null)
{
_work.Wait();
_work = null;
}
}
public void DoWorkLoop()
{
while (!_closeRequested.WaitOne(1000))
{
var last = _current;
var next = _last + _current;
if (next == 0)
{
next = 1;
}
_last = _current;
_current = next;
Console.WriteLine(next);
}
}
}
}
I’ve added some boiler-plate start/stop logic including a mutex to release the app and wait for it to finish in the stop command.
To run as a console app, I could simply wait for a keypress:
var app = new MyApp();
app.Start();
Console.ReadKey();
Console.WriteLine("Stopping");
app.Stop();
or I could make use of requiring ctrl+c to exit instead:
private static readonly AutoResetEvent _closing = new AutoResetEvent(false);
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
var app = new MyApp();
app.Start();
Console.CancelKeyPress += new ConsoleCancelEventHandler(OnExit);
_closing.WaitOne();
Console.WriteLine("Stopping");
app.Stop();
}
protected static void OnExit(object sender, ConsoleCancelEventArgs args)
{
Console.WriteLine("Exit Requested");
_closing.Set();
args.Cancel = true;
Console.CancelKeyPress -= new ConsoleCancelEventHandler(OnExit);
}
Either way, we now have a functioning console app, in a format that is compatible with a Windows Service.
Install the libraries
As mentioned, we will use PeterKottas/DotNetCore.WindowsService nuget package PeterKottas.DotNetCore.WindowsService to make our life easier. (dotnet add package PeterKottas.DotNetCore.WindowsService
)
The Program
Now we can change our program to start using the code from the library, instead of our code:
using System;
using System.Threading;
using PeterKottas.DotNetCore.WindowsService;
namespace MyApp
{
public class Program
{
static void Main(string[] args)
{
ServiceRunner<MyApp>.Run(config =>
{
var name = config.GetDefaultName();
config.SetName("MyAppService");
config.SetDescription("An example application");
config.SetDisplayName("MyApp As A Service");
config.Service(serviceConfig =>
{
serviceConfig.ServiceFactory((extraArguments, serviceController) =>
{
return new MyApp();
});
serviceConfig.OnStart((service, extraArguments) =>
{
Console.WriteLine("Service {0} started", name);
service.Start();
});
serviceConfig.OnStop(service =>
{
Console.WriteLine("Service {0} stopped", name);
service.Stop();
});
serviceConfig.OnInstall(service =>
{
Console.WriteLine("Service {0} installed", name);
});
serviceConfig.OnUnInstall(service =>
{
Console.WriteLine("Service {0} uninstalled", name);
});
serviceConfig.OnPause(service =>
{
Console.WriteLine("Service {0} paused", name);
});
serviceConfig.OnContinue(service =>
{
Console.WriteLine("Service {0} continued", name);
});
serviceConfig.OnError(e =>
{
Console.WriteLine("Service {0} errored with exception : {1}", name, e.Message);
});
});
});
}
}
}
I also had to add the IMicroService
interface to MyApp
, but otherwise it stayed the same since I already implemented the Start
/Stop
methods. Yes, it is a tonne more code, but thats just me logging state transitions, your app may not want or need to implement every event handler.
Now the app runs two ways:
dotnet run
Starting up as a console service host
Service MyApp.MyApp started
The MyAppService service is now running, press Control+C to exit.
1
1
2
3
5
8
13
21
Control+C detected, attempting to stop service.
Service MyApp.MyApp stopped
The MyAppService service has stopped.
And also installed as a service:
dotnet run action:install
Successfully registered and started service "MyAppService" ("An example application")
On Linux
In theory, we can take this app as written and build and run it as a console app on Linux. This is because all our code is portable dotnet core netstandard
and netcoreapp
cross-platform code. Yes, we have some interop code that expects some windows APIs, but in theory, if we never execute that code, it won’t cause any issues. Let’s find out.
The easiest way to run Linux on windows is probably docker, so we can test using that. (This assumes you have Docker installed and set up, otherwise, just follow along on any Linux environment you have.)
I am going to run the Microsoft/aspnetcore-build
image, so that the tools are available, and map the dev folder I was already using. I will just start a bash
shell so that I basically simulate working on my folder from a Linux machine. (Your networking may vary.)
docker run --rm -it -v "$(pwd):/app" -w /app microsoft/aspnetcore-build bash
This will likely spend some time pulling down the image if you haven’t used it before. Once that is done you will be dropped into a bash shell inside an instance of a Microsoft/aspnetcore-build
Linux container with the windows folder directory containing out application mapped to the /app
folder.
(As mentioned, if you don’t have docker, or would rather use a Linux environment you already have, the rest of the instructions should work much the same.)
All you need to do is build and run, and you should get a working application.
root@2683f31d537c:/app# ls
MyApp.cs MyApp.csproj Program.cs bin obj
root@2683f31d537c:/app# dotnet build
Microsoft (R) Build Engine version 15.6.84.34536 for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.
Restoring packages for /app/MyApp.csproj...
Installing System.ServiceProcess.ServiceController 4.4.0.
Installing DasMulli.Win32.ServiceUtils 1.0.1.
Installing PeterKottas.DotNetCore.CmdArgParser 1.0.5.
Installing PeterKottas.DotNetCore.WindowsService 2.0.6.
Generating MSBuild file /app/obj/MyApp.csproj.nuget.g.props.
Generating MSBuild file /app/obj/MyApp.csproj.nuget.g.targets.
Restore completed in 3.08 sec for /app/MyApp.csproj.
MyApp -> /app/bin/Debug/netcoreapp2.0/MyApp.dll
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:06.79
root@2683f31d537c:/app# dotnet run
Starting up as a console service host
Service MyApp.MyApp started
The MyAppService service is now running, press Control+C to exit.
1
1
2
3
5
^CControl+C detected, attempting to stop service.
Service MyApp.MyApp stopped
The MyAppService service has stopped.
And that’s it, the same code compiles on Linux as well, and runs successfully.
I’m not currently a Linux user and haven’t set up services or daemons for a while, so I will defer to others on the topic of Running a dotnet Core app as a Linux daemon
On Docker
Let’s whip up a Dockerfile to round it off, that will build and pack a new docker image, that we can then start and see it running our task.
First the docker file (Dockerfile). This is a basic minimalist version, you will likely want to do optimisation steps yourself.
FROM microsoft/dotnet:2.0-runtime AS base
WORKDIR /app
EXPOSE 80
FROM microsoft/aspnetcore-build:2.0 AS build
WORKDIR /src
COPY . .
RUN dotnet build -c Release -o /app
FROM build AS publish
RUN dotnet publish -c Release -o /app
FROM base AS final
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "MyApp.dll"]
This will use the Microsoft/aspnetcore-build:2.0
container image as the build container, publish the results and produce a packed container based on the Microsoft/dotnet:2.0-runtime
container image. We are also setting the container with an entry point to start the application process as the main container process. This means that if/when the process stops, the container terminates.
We run the build command, asking it to tag the created image as myapptestcontainer:latest
so we can refer to it again in a moment.
docker build -t myapptestcontainer:latest .
Sending build context to Docker daemon 136.7kB
Step 1/13 : FROM microsoft/dotnet:2.0-runtime AS base
---> 26314e3adaec
Step 2/13 : WORKDIR /app
Removing intermediate container 9296d10905ce
---> 8794c7aca866
Step 3/13 : EXPOSE 80
---> Running in 6554f663146f
Removing intermediate container 6554f663146f
---> ad881b2a405e
Step 4/13 : FROM microsoft/aspnetcore-build:2.0 AS build
---> 244f6193d21a
Step 5/13 : WORKDIR /src
Removing intermediate container a38fb58535b6
---> 5ed2a92fda93
Step 6/13 : COPY . .
---> 8ffd3faa7bc9
Step 7/13 : RUN dotnet build -c Release -o /app
---> Running in 39d44616ea2d
Microsoft (R) Build Engine version 15.6.82.30579 for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.
Restoring packages for /src/MyApp.csproj...
Installing System.ServiceProcess.ServiceController 4.4.0.
Installing PeterKottas.DotNetCore.CmdArgParser 1.0.5.
Installing DasMulli.Win32.ServiceUtils 1.0.1.
Installing PeterKottas.DotNetCore.WindowsService 2.0.6.
Generating MSBuild file /src/obj/MyApp.csproj.nuget.g.props.
Restore completed in 2.72 sec for /src/MyApp.csproj.
MyApp -> /app/MyApp.dll
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:06.14
Removing intermediate container 39d44616ea2d
---> 56b58883d64b
Step 8/13 : FROM build AS publish
---> 56b58883d64b
Step 9/13 : RUN dotnet publish -c Release -o /app
---> Running in 552eb703a748
Microsoft (R) Build Engine version 15.6.82.30579 for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.
Restore completed in 131.4 ms for /src/MyApp.csproj.
MyApp -> /src/bin/Release/netcoreapp2.0/MyApp.dll
MyApp -> /app/
Removing intermediate container 552eb703a748
---> 474498c42be3
Step 10/13 : FROM base AS final
---> ad881b2a405e
Step 11/13 : WORKDIR /app
Removing intermediate container c3c0dd6ac31c
---> 02c236862201
Step 12/13 : COPY --from=publish /app .
---> 33509849efe0
Step 13/13 : ENTRYPOINT ["dotnet", "MyApp.dll"]
---> Running in 25b373135eb6
Removing intermediate container 25b373135eb6
---> 511aa92712d1
Successfully built 511aa92712d1
Successfully tagged myapptestcontainer:latest
Now that we have a successful image for our app, we can start and run instances of it on docker, as well. We do this using the docker run
command.
As before, we can run this interactively using the -it command:
docker run --rm -it myapptestcontainer:latest
Starting up as a console service host
Service MyApp.MyApp started
The MyAppService service is now running, press Control+C to exit.
1
1
2
3
5
8
13
21
34
55
89
^CControl+C detected, attempting to stop service.
Service MyApp.MyApp stopped
The MyAppService service has stopped.
And Control+C still works as expected. The real proof is launching it and checking the processes. We will:
- Run an instance from our image, detached (
docker run -d myapptestcontainer:latest
) - See that it is running using list process (
docker ps
) - Stop the process (
docker stop <pid>
) - Print the logs (
docker logs <pid>
) - Clean up the process (
docker rm <pid>
)
> docker run -d myapptestcontainer:latest
5a11b3ee222d35196f7d7549d634cd8b8c9220bfb4f9dd9f7fd577b094b2bccb
> docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
5a11b3ee222d myapptestcontainer:latest "dotnet MyApp.dll" 2 seconds ago Up 1 second 80/tcp musing_montalcini
> docker stop 5a11b
5a11b
> docker logs 5a11b
Starting up as a console service host
Service MyApp.MyApp started
The MyAppService service is now running, press Control+C to exit.
1
1
2
3
5
> docker rm 5a11b
5a11b
As we can see, the only difference is the termination. Linux is sending the termination message correctly, but the library we are using doesn’t subscribe to the correct callback (AppDomain.CurrentDomain.ProcessExit
perhaps) and so instead the process is just terminated.
Now I started raising this as a bug against the library, but had to stop myself and ask “Do I really need this?” There are a bunch of reasons and ways your container could get terminated. You need to build in resilience for this termination. For that reason, you need to allow for you process to die in the middle of any part of your code and figure out ways to gracefully recover as needed. (Think about how SQL Server recovers after a termination to avoid data loss.) For this reason, I don’t see the fact that OnShutdown
doesn’t get called as a bug, but instead an opportunity to write a better process.
Of course, if you absolutely want this behaviour, you could do something like this stack overflow comment suggests and connect the handler yourself, calling into the appropriate function. Like replacing the Service factory with the following:
serviceConfig.ServiceFactory((extraArguments, serviceController) =>
{
var myApp = new MyApp();
EventHandler handler = null;
handler = (sender, _) =>
{
AppDomain.CurrentDomain.ProcessExit -= handler;
Console.WriteLine("Process Exit triggered", name);
myApp.Stop();
};
AppDomain.CurrentDomain.ProcessExit += handler;
return myApp;
});
Just make sure if you do decide to use this, that your stop function is idempotent and only runs once.
The End
I hope this article helps others coming to find out how to create cross-platform services with .Net Core. Also hopefully it redeems me for confusing so many people who landed on my Building a Windows Service with .Net Core article as well.
Simple. Easy. Works.