Worker Service
.NET finally feels like a complete framework - you can build RESTful APIs; Console Applications; Background Services; Web front-ends; and most importantly Windows Services - all without needing 3rd party software.
Worker Services are .NET's background service offering. It's the perfect way to create long running services, or services that need to fire at set intervals.
With the advent of .NET Core 3.0, Worker Services can now be created, and installed, as Windows Services without needing 3rd party tools like TopShelf.
Getting Started
The latest version of Visual Studio now comes with a Worker Service template - create a Worker Service Project from this template and call it SimpleFarm
.
A farm is a good example project to illustrate the concept of a worker services, because for a farm you need to
- do things at set intervals,
- and in a set order.
Setting up config
Firstly, we'll touch on the nifty way that .NET handles config. Create a Config folder in your project, and add the following class to it:
public class FarmConfig
{
public int TillTimeInMinutes { get; set; }
public int PlantTimeInMinutes { get; set; }
public int HarvestTimeInMinutes { get; set; }
public int FertileLandInMetersSquared { get; set; }
public string Crop { get; set; }
}
The appsettings.json
file is where you will be keeping all your config settings - update this file with the following entry:
...
"Farm": {
"TillTimeInMinutes": 3,
"PlantTimeInMinutes": 3,
"HarvestTimeInMinutes": 3,
"FertileLandInMetersSquared": 100,
"Crop": "Grain"
}
...
We're going to be planting grain, on a 100 square meter plot of land. We'll till the land for 3 minutes, plant the crop for 3 minutes, and harvest it in 3 minutes.
Setting up Processes
The farming logic won't be invoked directly in the Worker Service - rather, it will be moved to a set of classes I like to call processes. Create a Process folder in your project, and add the following interface and class to it:
public interface IFarmingProcess
{
Task TillTheLand();
Task PlantTheCrop();
Task HarvestTheCrop();
}
And
public class FarmingProcess : IFarmingProcess
{
private readonly FarmConfig config;
private readonly ILogger<FarmingProcess> logger;
public FarmingProcess(IOptions<FarmConfig> config, ILogger<FarmingProcess> logger)
{
this.config = config.Value;
this.logger = logger;
}
public async Task TillTheLand()
{
logger.LogInformation($"Tilling {config.FertileLandInMetersSquared} m^2 for {config.Crop} crop!!!");
await Task.Delay(TimeSpan.FromMinutes(config.TillTimeInMinutes));
}
public async Task PlantTheCrop()
{
logger.LogInformation($"Planting {config.FertileLandInMetersSquared} m^2 for {config.Crop} crop!!!");
await Task.Delay(TimeSpan.FromMinutes(config.PlantTimeInMinutes));
}
public async Task HarvestTheCrop()
{
logger.LogInformation($"Harvesting {config.FertileLandInMetersSquared} m^2 for {config.Crop} crop!!!");
await Task.Delay(TimeSpan.FromMinutes(config.HarvestTimeInMinutes));
}
}
The IOptions<FarmConfig> config
parameter in the constructor for FarmingProcess
is how .NET Core injects config. This will all be wired up once we define what goes into the Dependency Injection container in the Startup.cs
file.
Setting up the Worker
Now, we'll set up our Worker Service. Create a Worker folder and add the following class to it:
public class FarmWorker : BackgroundService
{
private readonly IFarmingProcess farmingProcess;
private readonly ILogger<FarmWorker> logger;
public FarmWorker(IFarmingProcess farmingProcess, ILogger<FarmWorker> logger)
{
this.farmingProcess = farmingProcess;
this.logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// This is how we keep the app running (in the background)
while (!stoppingToken.IsCancellationRequested)
{
logger.LogInformation("FarmWorker running at: {time}", DateTimeOffset.Now);
await farmingProcess.TillTheLand();
await farmingProcess.PlantTheCrop();
await farmingProcess.HarvestTheCrop();
}
}
}
This is the Worker Service - and it is created by inheriting the BackgroundService
class, and overriding ExecuteAsync(...)
.
When the application runs, it will execute ExecuteAsync(...)
in the background. We ensure that ExecuteAsync(...)
keeps running/processing its task by wrapping it in a while (!stoppingToken.IsCancellationRequested) {...}
loop, which only ends once the application terminates (and cancellation is requested).
Setting up the Application Host
Finally, we set up the host. Add the following Nuget package to your project, to allow it to be installed as a Windows Service:
Microsoft.Extensions.Hosting.WindowsServices
Now remove everything from the Program.cs
file and replace with the following:
public class Program
{
public static void Main(string[] args) => CreateHostBuilder(args).Build().Run();
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddConsole();
logging.AddEventLog();
})
// Essential to run this as a window service
.UseWindowsService()
.ConfigureServices(configureServices);
private static void configureServices(HostBuilderContext context, IServiceCollection services)
{
services.Configure<FarmConfig>(context.Configuration.GetSection("Farm"));
services.AddLogging();
services.AddSingleton<IFarmingProcess, FarmingProcess>();
services.AddHostedService<FarmWorker>();
}
}
lastly, add the following to the "Logging"
property in your appsettings.json
,
"Logging": {
...
"EventLog": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
}
},
There are 4 really important things happening here.
1.) .UseWindowsService()
allows your worker service to be installed as a Windows Service.
2.) context.Configuration
is how you access the configuration that you set within the appsettings.json
file.
3.) services.AddHostedService<FarmWorker>()
is how you hook your worker service into the application Host process.
4.) logging.AddEventLog()
logs messages directly to the Windows Event Log, so we can see if your Worker Service is actually working.
It's crucial that we update the appsettings.json
to configure logging to the Windows Event Log.
Running the application, the console should look similar to the following, after all three farming processes have been completed:
Installing as a Windows Service
Great! We now have a Worker Service that can process data at set intervals, execute some SQL cleanup commands, or even do some farming!
This worker service can be attached to any running .NET Core host - an ASP.NET Core API might be a good place to do this, or it can be conveniently installed as a Windows Service and run all on it's own.
To install this as a Windows Service, run a command line shell from the root of your solution and execute
dotnet build
Secondly, use the Service Create (sc
) tool on Windows to install your worker service as a Windows Service. I recommend using PowerShell for this. Run your command line shell as Administrator and execute the follow script/command:
sc.exe create FarmWorkerService binPath=C:\...\path to your solution\SimpleFarm\SimpleFarm\bin\Debug\netcoreapp3.1\SimpleFarm.exe
Your service has been created! but it's not running just yet.
Open the Services app on Windows, and look for your service by name - FarmWorkerService. You should see something similar to this.
You can manage the FarmWorkerService through the Services UI or you can manage it through the console with the following 3 commands
-
sc.exe start DemoWorkerService
start the service -
sc.exe stop DemoWorkerService
stops the service -
sc.exe delete DemoWorkerService
deletes the service
Either start your service using the Service Create command line tool, or start it using the Services UI. Your FarmWorkerService should now be running!
To confirm that your service is actually running, you can open the Event Viewer app on windows and...
See it happily plodding along!
Top comments (1)
Thanks for sharing!!