Creating a Windows service used to be a cumbersome process but has become much easier with the new worker service framework introduced in .NET Core 3.0. The code in this article is mostly to introduce the reader to worker services and explain how to deploy a worker service as a Windows services, but the application hopefully also has some real usefulness to it in itself.
Disclaimer
The service we are creating here is used for removing files older than a specific date from a list of folders specified in a stand-alone configuration file. This application has been tested to the extent that I know that it works in my own environment. I don’t give any guarantees that it will work flawless in yours. Before using this in a production environment, I strongly suggest that you perform some additional testing.
Introduction
In this article, I will go through how you can create a simple Windows Service using the Worker Service template in Visual Studio.
Before Worker Services in .NET Core 3.0 was introduced, you would probably create your Windows services using the .NET Windows Service template. I always thought that the experience of using that template never was very intuitive. The first issue you might encounter is that you can’t run the service within your Visual Studio environment without implementing some code hacks. Also, the template didn’t give you much guidance for what is actually needed to make the service run as expected. Fortunately, with .NET 3.0 and Worker Services, all of this has become much easier.
The Solution
To follow this article, you need to have Visual Studio 2019 and .NET Core 3.0 installed on your environment.
First, you need to create a new project and choose the Worker Process template as outlined in the image below:
Adding Configuration Settings
In the appsettings.json file, we will need to add a few configurations. One configuration for the threshold stating the max allowed age for the file before we delete it. Let’s call this setting NumberOfDaysBeforeDelete
. I set the value for this to 90 days. Besides this, we need the file path to the stand-alone configuration file containing the list of folders to monitor. I call this setting ConfigurationFilePath
and point that to a location in my c:\temp folder. The last setting is needed for configuring the interval for which this service will be executing our custom code. I call this setting RunIntervallInHours
and I set the value to 12 hours. This means that this service will check the content of each folder in the configuration file twice a day and remove all files with a last written date of more than 90 days. These three configuration settings are grouped into a node in the appsettings.json file which I call “App.Configurations
”, see here below:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"App.Configurations": {
"ConfigurationFilePath": "C:\\Temp\\CleanUpFolders\\CleanUpConfigurations.config",
"NumberOfDaysBeforeDelete": "10",
"RunIntervallInHours": "12"
}
}
Please note that you have both an “appsetttings.json” file as well as an “appsettings.development.json” file in your project. Which appsettings.json file is used during runtime depends on your environmental settings. When running this service from Visual Studio, it will default to use the development settings as configured in the file Properties/launchSettings.json.
Add Configuration Files With List of Folders
I create a file named CleanUpConfigurations.config in c:\Temp\CleauUpFoders\ and put the following two lines in it:
C:\Temp\CleanUpFolders\Folder1
C:\Temp\CleanUpFolders\Folder2
I also create the folders “Folder1” and “Folder2” in the same location.
Our Code
In the newly created solution, two files are initially of interest to us, program.cs used to start the application and worker.cs which will contain our custom code for this solution. In the program.cs file, you will find code for setting up the host environment. To facilitate running this application as a Windows service, you need to add the following NuGet package.
Install-Package Microsoft.Extensions.Hosting.WindowsServices
With that package installed, you can add the row .UseWindowsService()
after the Host.CreateDefaultBuilder(args)
, so that you end up with the following in your program.cs file.
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseWindowsService()
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<worker>();
});
}
In the worker.cs file, we start by injecting an instance of IServiceScopeFactory
to the constructor. This will be used to access the appsettings configuration file.
public Worker(ILogger<worker> logger, IServiceScopeFactory serviceScopeFactory)
{
_logger = logger;
_serviceScopeFactory = serviceScopeFactory;
}
To make sure that configurations are read every time the service is started, I override the method StartAsync
and retrieve my application configurations here:
public override Task StartAsync(CancellationToken cancellationToken)
{
_configuration = _serviceScopeFactory.CreateScope().
ServiceProvider.GetRequiredService<iconfiguration>();
_numberOfDaysBeforeDelete = int.Parse(_configuration
["App.Configurations:NumberOfDaysBeforeDelete"]);
_runIntervallInHours = int.Parse(_configuration
["App.Configurations:RunIntervallInHours"]);
_folderPaths = File.ReadAllLines(_configuration
["App.Configurations:ConfigurationFilePath"]).Select(x => x.Trim()).ToList();
return base.StartAsync(cancellationToken);
}
Note how I am reading all lines from the file stated in the ConfigurationFilePath
setting and put them in the list _folderPaths
.
The following variables are declared in the Worker
class:
private IList<string> _folderPaths;
private int _numberOfDaysBeforeDelete;
private int _runIntervallInHours;
Our code for retrieving folders from our configuration file and remove all files older than 90 days will be placed in the method ExecuteAsync
. ExecuteAsync
takes a CancellationToken
as a parameter. The CancellationToken
is very important because it will be used to identify when the service is stopped. When this happens, the property IsCancellationRequested
will be set to true
. Hence, the first line of code in our ExecuteAsync
is:
while (!stoppingToken.IsCancellationRequested)
I will do a second check for IsCancellationRequested
in this method further down to make sure that I stop the execution as quickly as possible when receiving a cancellation request. The code for getting files older than x number of days and deleting them are written in two lines:
var files = Directory.GetFiles(path).Select(file => new FileInfo(file)).Where
(file => file.LastWriteTime < DateTime.Now.AddDays(-1* _numberOfDaysBeforeDelete)).ToList();
files.ForEach(file => file.Delete());
Before closing the while
loop, I put in a delay. Since our setting _runIntervallInHours
is set to 12 hours, this code will put the service to rest for 12 hours before continuing the execution.
await Task.Delay(TimeSpan.FromHours(_runIntervallInHours), stoppingToken);
Notice the use of the cancellation token here which causes the waiting process to be cancelled when the windows service stops.
The complete code for the ExecuteAsync
method will look like this:
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
foreach (var path in _folderPaths)
{
if (!stoppingToken.IsCancellationRequested)
{
var files = Directory.GetFiles(path).Select
(file => new FileInfo(file)).Where
(file => file.LastWriteTime < DateTime.Now.AddDays
(-1* _numberOfDaysBeforeDelete)).ToList();
files.ForEach(file => file.Delete());
}
}
await Task.Delay(TimeSpan.FromHours(_runIntervallInHours), stoppingToken);
}
}
That is all the code we need. You can simply press F5 and run the application directly from Visual Studio and see that it works.
Turning the Application Into a Windows Service
Now go forward and publish the application.
To a folder:
Open up a PowerShell terminal as an administrator and run the following command:
sc.exe create FolderCleanUpService binpath= "C:\Apps\FolderCleanUpService.exe" start= auto
This will install the service on your Windows machine.
You can put your service anywhere you want, I choose here to put it in the Apps folder on my c drive. I use the start=auto
flag to make sure that the service is automatically started when Windows starts. To set a description on the service, you need to execute a second command:
sc.exe description "FolderCleanUpService"
"This service monitors and deletes files older than 90 days"
Press (Window + R) and run the command services.msc, then you should see a list of all your services and in that list, you should be able to see the FolderCleanUpService
service. You need to manually start it the first time, but since the startup type is set to automatic, it will automatically start up itself after a reboot.
You can remove the service using this command:
sc.exe delete "FolderCleanUpService"
History
- 3rd April, 2020: Initial version