Using the Worker template now is possible to host an ASP.Net Core 3.1 Worker Service as a Windows Service.
This is all new to me, and for most of us, creating Windows Services using the old .Net Framework’s “Windows Service” is all we had done for years.
It is amazing to see .Net Core pushing all these changes as it matures as a framework.
Nonetheless, there are some growing pains (even migrating from older versions of .Net Core).
So, I decided to create a basic Windows Service application using the latest (as of September 2020) .Net Core 3.1 and Visual Studio 2019.
Hopefully can save some other programmer some time (if that is so, please just share the article. Thanks)
Requirements
Let’s list the small list of framework versions and tools used to host an ASP.Net Core 3.1 application as a Windows service:
- My PC is running Windows 10.
- Visual Studio 2019 Version 16.7.2
- .NET Core 3.1
Creating a ASP.NET Core 3.1 Worker Service
- On Visual Studio 2019, create a new project.
- You can select “Service” from the project type dropdown, that would make it easier to choose the correct service template.
- Now you can see both, our good old friend “Windows Service (.NET Framework) and the new”Worker Service”, choose the last one.
- On the next screen, enter the name (“SampleWorkerService”) and the location of your code.
- Click “Create”
That is all we need to create a basic Worker Service project.
But, is not a Windows Service yet.
We still need to add some dependencies.
In this sample project, I would also add Log4Net logging.
Making the Worker Service a Windows Service
What we have at this moment is a .NET Core 3.1 Worker service.
Not a Windows Service yet.
For that, we need to add a package reference to our project and make some changes to our code.
- Right-click on your project in Solution Explorer.
- Click on “Manage NuGet Packages”.
- On the NuGet Package Manager, you can write “services” in the search box to help you reduce the results.
- Select “Microsoft.Extensions.Hosting.WindowsServices” (version at this time is 3.1.7)
This extension is what allows the IHostBuilder to behave like a Windows service.
Now, in Program.cs you just need to make use of “UseWindowsService()” to add the behavior.
No worries, I would add the full code at the end.
Now is a Windows Service.
Let’s not publish and deploy as a Windows Service just yet, but instead, let’s talk a bit about the Worker sample application.
.NET Core Worker Service, Timer and Tasks
In many of the Windows services I had coded is always a timer involved.
One typical requirement is to execute some work every 10 minutes for example.
That could be easily solved by adding a Timer.
Another requirement is to be able to spawn a limited number of tasks within that time space.
For example, in my sample code I followed these requirements:
- Only one task can be executing the main operation at any time (MAX_TASKS = 2).
- The timer will wake up every 4 seconds and raise the main event “DoWork”.
- The amount of time “DoWork” will take to complete its job is around 10 seconds.
You see what is going to happen here, right?
You could have 1 or more tasks ready to jump into the main operation at one time and we need to restrict that to just one at a time.
For that, we use “lock”, to lock just the amount of work that needs to remain exclusive to one thread.
To make sure, we have a counter called “id” that will only be used and increased within the lock space.
A variable “taskCount” makes sure we only run the specified number of tasks.
Based on the requirements, we will have attempts to create a new task every 4 seconds, but the variable “taskCount” will take care of not allowing more than the maximum number of tasks at any time.
Let’s show the code now, and then we can talk about adding Log4Net.
Basically, there were changes to only 2 files:
Program.cs
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace SampleWorkerService { public class Program { public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .UseWindowsService() .ConfigureLogging((hostingContext, config) => { config.AddLog4Net("log4net.config", true); config.SetMinimumLevel(LogLevel.Debug); }) .ConfigureServices((hostContext, services) => { services.AddHostedService<Worker>(); }); } }
and Worker.cs
using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace SampleWorkerService { public class Worker : BackgroundService { private Timer _timer; private const int MAX_TASKS = 2; private int taskCount = 0; private static readonly object _lock = new object(); private const int TIMER_AWAKE_EVERY_SECONDS = 4; private const int SLEEP_IN_SECONDS = 10; // used to fake work being done. private readonly ILogger<Worker> _logger; int id = 0; public Worker(ILogger<Worker> logger) { _logger = logger; } protected override Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("ExecuteAsync started."); _timer = new Timer(DoWork, stoppingToken, TimeSpan.Zero, TimeSpan.FromSeconds(TIMER_AWAKE_EVERY_SECONDS)); return Task.CompletedTask; } private void DoWork(object state) { _logger.LogInformation($"DoWork work started at {DateTime.Now}"); CancellationToken cancellationToken = (CancellationToken)state; if (taskCount < MAX_TASKS) { taskCount++; Task t = Task.Run(() => { _logger.LogInformation($"DoWork new Task: [{Task.CurrentId.Value}] spawned."); // Exclusive operation starts here lock (_lock) { _logger.LogInformation($"Sleeping inside lock for {SLEEP_IN_SECONDS} seconds. ID={id}"); Thread.Sleep(SLEEP_IN_SECONDS * 1000); // This is where all your work goes. _logger.LogInformation($"Waking up inside lock. ID={id}"); id++; } }, cancellationToken); t.Wait(cancellationToken); taskCount--; } else { _logger.LogInformation($"It was time to start a new task, but was denied. Max number of tasks ({MAX_TASKS}) already reach."); } _logger.LogInformation($"DoWork work completed at {DateTime.Now}"); } } }
Using the Right Timer Constructor to Pass Parameters to the Callback Function
This part was a little tricky for me and I spent quite some time finding a good solution.
The problem was that I needed to pass a parameter (CancellationToken) to the TimerCallback function.
At the end this was the perfect solution:
_timer = new Timer(DoWork, stoppingToken, TimeSpan.Zero, TimeSpan.FromSeconds(TIMER_AWAKE_EVERY_SECONDS));
The second parameter is the CancellationToken (“stoppingToken” variable” injected in ExecuteAsync).
The trick was to cast the object (“state”) to the correct type (“CancellationToken “) once inside the callback function:
private void DoWork(object state) { _logger.LogInformation($"DoWork work started at {DateTime.Now}"); CancellationToken cancellationToken = (CancellationToken)state;
Definitely something new to me.
Adding Log4Net to .NET Core 3.1 Worker Service
The problem with .Net Core right now is that the logger that is included and baked into the framework does not logs to a file.
“Old school” loves log files, right?
First, we have to add a new package to our project: Microsoft.Extensions.Logging.Log4Net.AspNetCore (version 3.1.0)
Next, configure the Host to use Log4Net using “ConfigureLogging”, like this:
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .UseWindowsService() .ConfigureLogging((hostingContext, config) => { config.AddLog4Net("log4net.config", true); config.SetMinimumLevel(LogLevel.Debug); }) .ConfigureServices((hostContext, services) => { services.AddHostedService<Worker>(); });
No changes to Worker.cs.
Already is set to use ILogger from Microsoft.Extensions.Logging and using dependency injection to make it available to this class.
public Worker(ILogger<Worker> logger) { _logger = logger; }
The last thing to do is create the Log4Net configuration.
Already in “config.AddLog4Net” we are passing the name and location (root) of this configuration file: log4net.config
A basic one will do for now.
<?xml version="1.0" encoding="utf-8"?> <log4net> <root> <level value="ALL" /> <appender-ref ref="file" /> </root> <appender name="file" type="log4net.Appender.RollingFileAppender"> <file value="SampleWorkerService.log" /> <appendToFile value="true" /> <rollingStyle value="Size" /> <maxSizeRollBackups value="5" /> <maximumFileSize value="25MB" /> <staticLogFileName value="true" /> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%date [%thread] %level %logger - %message%newline" /> </layout> </appender> </log4net>
I had problems on Windows 10 creating the log file on the root of the install, so I later changed the “file value” to the location of my published code: “C:\Publish\SampleWorkerService\SampleWorkerService.log”
That’s it. Logging on Net Core 3.1 using Log4Net added.
Publishing your .NET Core 3.1 Worker Service
Let’s get our code ready to be deploy as a Windows service, but first we have to publish our code.
For that just right-click on your main project, and select “Publish…”
You have to choose a target location (“c:\Publish\SampleWorkerService”).
- Configuration set to “release”.
- Target framework is “netcoreapp3.1”.
- Target runtime leave it as “Portable”.
- Then hit the button “Publish”
Now we have the files we need to create and install this NET Core 3.1 Worker Service as a Windows Service.
How to Install your NET Core 3.1 Worker Service as a Windows Service
To create and install your NET Core 3.1 worker service in your computer as a Windows service you will need to use the “sc” command.
You can create a Powershell script or a batch file, I decided to run from an Administrator CMD command window.
Make sure you are opening or running your script or batch as an Administrator, otherwise you might encounter trouble when trying to install the service.
This simple line will create and install my sample worker service application as a Windows service:
C:\Publish\SampleWorkerService>sc create SampleWorkerService binPath="c:\publish\sampleworkerservice\sampleworkerservice.exe" [SC] CreateService SUCCESS
The returning line: “[SC] CreateService SUCCESS” tells you everything went well.
Now you should be able to open your “Services” window and run the service.
Right-click “Start” or click the green arrow on the menu for the service to start.
What Are We Logging?
Here is a sample of the log produced by the NET Core 3.1 Worker Service installed and running as a Windows Service:
2020-09-02 16:02:58,536 [1] INFO SampleWorkerService.Worker - ExecuteAsync started. 2020-09-02 16:02:58,552 [1] INFO Microsoft.Hosting.Lifetime - Application started. Press Ctrl+C to shut down. 2020-09-02 16:02:58,556 [1] INFO Microsoft.Hosting.Lifetime - Hosting environment: Development 2020-09-02 16:02:58,557 [4] INFO SampleWorkerService.Worker - DoWork work started at 9/2/2020 4:02:58 PM 2020-09-02 16:02:58,558 [1] INFO Microsoft.Hosting.Lifetime - Content root path: C:\Repository\SampleWorkerService\SampleWorkerService 2020-09-02 16:02:58,579 [8] INFO SampleWorkerService.Worker - DoWork new Task: [5] spawned. 2020-09-02 16:02:58,582 [8] INFO SampleWorkerService.Worker - Sleeping inside lock for 10 seconds. ID=0 2020-09-02 16:03:02,552 [10] INFO SampleWorkerService.Worker - DoWork work started at 9/2/2020 4:03:02 PM 2020-09-02 16:03:02,563 [11] INFO SampleWorkerService.Worker - DoWork new Task: [9] spawned. 2020-09-02 16:03:06,547 [9] INFO SampleWorkerService.Worker - DoWork work started at 9/2/2020 4:03:06 PM 2020-09-02 16:03:06,548 [9] INFO SampleWorkerService.Worker - It was time to spawn a thread, but was denied. Max number of threads (2) already reach. 2020-09-02 16:03:06,549 [9] INFO SampleWorkerService.Worker - DoWork work completed at 9/2/2020 4:03:06 PM 2020-09-02 16:03:08,584 [8] INFO SampleWorkerService.Worker - Waking up inside lock. ID=0 2020-09-02 16:03:08,586 [11] INFO SampleWorkerService.Worker - Sleeping inside lock for 10 seconds. ID=1 2020-09-02 16:03:08,589 [4] INFO SampleWorkerService.Worker - DoWork work completed at 9/2/2020 4:03:08 PM 2020-09-02 16:03:10,559 [12] INFO SampleWorkerService.Worker - DoWork work started at 9/2/2020 4:03:10 PM 2020-09-02 16:03:10,563 [4] INFO SampleWorkerService.Worker - DoWork new Task: [10] spawned. 2020-09-02 16:03:14,561 [8] INFO SampleWorkerService.Worker - DoWork work started at 9/2/2020 4:03:14 PM 2020-09-02 16:03:14,563 [8] INFO SampleWorkerService.Worker - It was time to spawn a thread, but was denied. Max number of threads (2) already reach. 2020-09-02 16:03:14,565 [8] INFO SampleWorkerService.Worker - DoWork work completed at 9/2/2020 4:03:14 PM 2020-09-02 16:03:18,548 [8] INFO SampleWorkerService.Worker - DoWork work started at 9/2/2020 4:03:18 PM 2020-09-02 16:03:18,549 [8] INFO SampleWorkerService.Worker - It was time to spawn a thread, but was denied. Max number of threads (2) already reach. 2020-09-02 16:03:18,550 [8] INFO SampleWorkerService.Worker - DoWork work completed at 9/2/2020 4:03:18 PM 2020-09-02 16:03:18,589 [11] INFO SampleWorkerService.Worker - Waking up inside lock. ID=1 2020-09-02 16:03:18,592 [4] INFO SampleWorkerService.Worker - Sleeping inside lock for 10 seconds. ID=2 2020-09-02 16:03:18,593 [10] INFO SampleWorkerService.Worker - DoWork work completed at 9/2/2020 4:03:18 PM 2020-09-02 16:03:22,547 [11] INFO SampleWorkerService.Worker - DoWork work started at 9/2/2020 4:03:22 PM 2020-09-02 16:03:22,549 [8] INFO SampleWorkerService.Worker - DoWork new Task: [11] spawned. 2020-09-02 16:03:26,547 [9] INFO SampleWorkerService.Worker - DoWork work started at 9/2/2020 4:03:26 PM 2020-09-02 16:03:26,549 [9] INFO SampleWorkerService.Worker - It was time to spawn a thread, but was denied. Max number of threads (2) already reach. 2020-09-02 16:03:26,550 [9] INFO SampleWorkerService.Worker - DoWork work completed at 9/2/2020 4:03:26 PM 2020-09-02 16:03:28,596 [4] INFO SampleWorkerService.Worker - Waking up inside lock. ID=2 2020-09-02 16:03:28,598 [8] INFO SampleWorkerService.Worker - Sleeping inside lock for 10 seconds. ID=3 2020-09-02 16:03:28,599 [12] INFO SampleWorkerService.Worker - DoWork work completed at 9/2/2020 4:03:28 PM 2020-09-02 16:03:30,547 [4] INFO SampleWorkerService.Worker - DoWork work started at 9/2/2020 4:03:30 PM 2020-09-02 16:03:30,550 [10] INFO SampleWorkerService.Worker - DoWork new Task: [12] spawned. 2020-09-02 16:03:34,551 [12] INFO SampleWorkerService.Worker - DoWork work started at 9/2/2020 4:03:34 PM 2020-09-02 16:03:34,556 [12] INFO SampleWorkerService.Worker - It was time to spawn a thread, but was denied. Max number of threads (2) already reach. 2020-09-02 16:03:34,561 [12] INFO SampleWorkerService.Worker - DoWork work completed at 9/2/2020 4:03:34 PM 2020-09-02 16:03:38,554 [9] INFO SampleWorkerService.Worker - DoWork work started at 9/2/2020 4:03:38 PM 2020-09-02 16:03:38,560 [9] INFO SampleWorkerService.Worker - It was time to spawn a thread, but was denied. Max number of threads (2) already reach. 2020-09-02 16:03:38,565 [9] INFO SampleWorkerService.Worker - DoWork work completed at 9/2/2020 4:03:38 PM 2020-09-02 16:03:38,603 [8] INFO SampleWorkerService.Worker - Waking up inside lock. ID=3 2020-09-02 16:03:38,606 [10] INFO SampleWorkerService.Worker - Sleeping inside lock for 10 seconds. ID=4 2020-09-02 16:03:38,609 [11] INFO SampleWorkerService.Worker - DoWork work completed at 9/2/2020 4:03:38 PM 2020-09-02 16:03:42,548 [11] INFO SampleWorkerService.Worker - DoWork work started at 9/2/2020 4:03:42 PM 2020-09-02 16:03:42,550 [9] INFO SampleWorkerService.Worker - DoWork new Task: [13] spawned. 2020-09-02 16:03:46,547 [8] INFO SampleWorkerService.Worker - DoWork work started at 9/2/2020 4:03:46 PM 2020-09-02 16:03:46,549 [8] INFO SampleWorkerService.Worker - It was time to spawn a thread, but was denied. Max number of threads (2) already reach. 2020-09-02 16:03:46,551 [8] INFO SampleWorkerService.Worker - DoWork work completed at 9/2/2020 4:03:46 PM 2020-09-02 16:03:48,608 [10] INFO SampleWorkerService.Worker - Waking up inside lock. ID=4 2020-09-02 16:03:48,609 [9] INFO SampleWorkerService.Worker - Sleeping inside lock for 10 seconds. ID=5 2020-09-02 16:03:48,611 [4] INFO SampleWorkerService.Worker - DoWork work completed at 9/2/2020 4:03:48 PM 2020-09-02 16:03:50,549 [4] INFO SampleWorkerService.Worker - DoWork work started at 9/2/2020 4:03:50 PM 2020-09-02 16:03:50,553 [12] INFO SampleWorkerService.Worker - DoWork new Task: [14] spawned.
The most important thing to notice is that, regardless of the amount of tasks spawned (just 2 this time), the IDs corresponding to the “lock” area are always in sync.
When one ID goes to sleep, no other ID enters the same lock space.
Everything waits until the same ID wakes up.
The next task enters the lock section immediately, because it has already been spawned and is just outside waiting for its turn.
Last Words
I coded a similar application years ago, before NET Core adopted the same “windows service as a console application running on a service host” approach.
I liked it before (it was really easy to debug) and I like it now.
You can easily debug the same code from your Visual Studio debugging environment or as a Windows Service running in your Windows PC.
Like I said before, there are some growing pains, even for those programmers already familiar with NET Core 2.x, but is worth it.
Hopefully this article is able to clear some things.
If you have any questions, any comments, any ideas on how to improve this code, please just send it to me using the comment form below.
If you like the article, please share it with your friends in social media. Thanks.
Florian
Hi David!
Nice example, thats what I was looking 4.
Could you provide a download please?
Thanks Florian