Note, this tutorial is about hosting an ASP.NET Core web app as a windows service, specifically in .NET Core 3.
If you are looking to host a web app as a service in .NET Core 2, check out this other tutorial : Hosting An ASP.NET Core Web Application As A Windows Service In .NET Core 2
If you are looking to run a Windows Service as a “worker” or for background tasks, then you’ll want this tutorial : Creating Windows Services In .NET Core – Part 3 – The “.NET Core Worker” Way
This is actually somewhat of a duplicate of a previous post I did here. But that was using .NET Core 2+, and since then, things have changed quite a bit. Well… Enough that when I tried to follow my own tutorial recently I was wondering what the hell I was going on about when nothing worked for me this time around.
Why A Web App As A Windows Service
So this tutorial is about running a Web App as a Windows Service. Why would that ever be the case? Why would you not have a web app running under something like IIS? Or why a Windows Service specifically?
Well the answer to why not under IIS is that in some cases you may not have IIS on the machine. Or you may have IIS but it’s not set up to host .NET Core apps anyway. In these cases you can do what’s called a self contained deploy (Which we’ll talk about soon), where the web app runs basically as an exe that you can double click and suddenly you have a fully fledged web server up and running – and portable too.
For the latter, why a windows service? Well if we follow the above logic and we have an exe that we can just click to run, then a windows service just gives us the ability to run on startup, run in the “background” etc. I mean, that’s basically all windows services are right? Just the OS running apps on startup and in the background.
Running Our Web App As A Service
The first thing we need to do is make our app compile down to an EXE. Well.. We don’t have to but it makes things a heck of a lot easier. To do that, we just need to edit our csproj and add the OutputType of exe. It might end up looking like so :
In previous versions of .NET Core you had to install the package Microsoft.AspNetCore.Hosting.WindowsServices , however as of right now with .NET Core 3+, you instead need to use Microsoft.Extensions.Hosting.WindowsServices . I tried searching around for when the change happened, and why, and maybe information about differences but other than opening up the source code I couldn’t find much out there. For now, take my word on it. We need to install the following package into our Web App :
Now there is just a single line we need to edit. Inside program.cs, you should have a “CreateHostBuilder” method. You might already have some custom configuration going on, but you just need to tack onto the end “UseWindowsServices()”.
And that’s all the code changes required!
Deploying Our Service
… But we are obviously not done yet. We need to deploy our service right!
Open a command prompt as an Administrator, and run the following command in your project folder to publish your project :
dotnet publish -c Release
Next we can use standard Windows Service commands to install our EXE as a service. So move your command prompt to your output folder (Probably along the lines of C:\myproject\bin\Release\netcoreapp3.0\publish). And run something like so to install as a service :
sc create MyApplicationWindowsService binPath= myapplication.exe
Doing the full install is usually pretty annoying to do time and time again, so what I normally do is create an install.bat and uninstall.bat in the root of my project to run a set of commands to install/uninstall. A quick note when creating these files. Create them in something like Notepad++ to ensure that the file type is UTF8 *without BOM*. Otherwise you get all sorts of weird errors :
The contents of my install.bat file looks like :
sc create MyService binPath= %~dp0MyService.exe
sc failure MyService actions= restart/60000/restart/60000/""/60000 reset= 86400
sc start MyService
sc config MyService start=auto
Keep the weird %~dp0 character there as that tells the batch process the current directory (Weird I know!).
And the uninstall.bat :
sc stop MyService
timeout /t 5 /nobreak > NUL
sc delete MyService
Ensure these files are set to copy if newer in Visual Studio, and now when you publish your project, you only need to run the .bat files from an administrator command prompt and you are good to go!
Doing A Self Contained Deploy
We talked about it earlier that the entire reason for running the Web App as a Windows Service is so that we don’t have to install additional tools on the machine. But that only works if we are doing what’s called a “self contained” deploy. That means we deploy everything that the app requires to run right there in the publish folder rather than having to install the .NET Core runtime on the target machine.
All we need to do is run our dotnet release command with a few extra flags :
dotnet publish -c Release -r win-x64 --self-contained
This tells the .NET Core SDK that we want to release as self contained, and it’s for Windows.
Your output path will change from bin\Release\netcoreapp3.0\publish to \bin\Release\netcoreapp3.0\win-x64\publish
You’ll also note the huge amount of files in this new output directory and the size in general of the folder. But when you think about it, yeah, we are deploying the entire runtime so it should be this large.
The fact that .NET Core is open source literally saves hours of debugging every single time I work on a greenfields project, and this time around is no different. I took a quick look at the actual source code of what the call to UseWindowsService does here. What I noticed is that it sets the content root specifically for when it’s running under a Windows Service. I wondered how this would work if I was reading a local file from disk inside my app, while running as a Windows Service. Normally I would just write something like :
But… Obviously there is something special when running under a Windows Service context. So I tried it out and my API bombed. I had to check the Event Viewer on my machine and I found :
Exception Info: System.IO.FileNotFoundException: Could not find file 'C:\WINDOWS\system32\myfile.json'.
OK. So it looks like when running as a Windows Service, the “root” of my app thinks it’s inside System32. Oof. But, again, looking at the source code from Microsoft gave me the solution. I can simply use the same way they set the content root to load my file from the correct location :
And we are back up and running!