dotWork is a library aimed to ease the development of Worker Services in .NET. More specifically, it extends an existing BackgroundService class in order to reduce the number of a boilerplate code required to set up a Worker Service.
You should consider using dotWork if you find yourself repeating the same boilerplate code required to set up simple and complex works alike.
To create a new Worker Service project with Visual Studio, you'd select File > New > Project.... From the Create a new project dialog search for "Worker Service", and select Worker Service template. If you'd rather use the .NET CLI, open your favorite terminal in a working directory. Run the dotnet new
command, and replace the <Project.Name>
with your desired project name.
dotnet new worker --name <Project.Name>
Please note that the worker project template automatically creates an example work class. You should delete it and create a new one by following the instructions below.
Install the dotWork
package by following the instructions.
dotnet add package dotWork
At this point the dotWork package is installed in your project but is not used. dotWork will not have any side-effects on your application unless explicitly used.
The first step of using dotWork is creating a work class itself.
using dotWork;
...
public class ExampleWork : WorkBase<DefaultWorkOptions>
{
}
There are two things to notice:
- You need to add a
using dotWork
directive to be able access theWorkBase
type. - You need to inherit your work class from
WorkBase<TWorkOptions>
, whereTWorkOptions
can be any class that implementsIWorkOptions
interface. For simplicity, in this example we will useDefaultWorkOptions
class that is provided alongside the library.
Now we need to write a method that describes what is needed to be done in each work iteration. dotWork will use this method to periodically execute a work iteration. We will learn how we can configure a delay between iterations later.
For dotWork to be able to find the iteration method it needs to satisfy a number of conditions:
- Have a name
ExecuteIteration
. - Be
public
. - Return either
void
orTask
.
Let's create one:
public class ExampleWork : WorkBase<DefaultWorkOptions>
{
public async Task ExecuteIteration(CancellationToken ct)
{
await Task.Delay(TimeSpan.FromSeconds(1), ct);
Console.WriteLine("Work iteration finished!");
}
}
This is a simple iteration that waits for a second and then prints to console. Notice that we have specified a CancellationToken ct
argument. It will be automatically provided by dotWork and will be triggered once the application host starts performing a shutdown.
Note that the CancellationToken
is not the only argument you can specify in the iteration method definition. In fact, you can specify any reference type as an argument and dotWork will try to resolve it from an application's service provider in a scoped manner. More on that later.
Once we're finished creating a work class we also need to register it with our host container. We can do it by adding the following line to our HostBuilder's ConfigureServices
method:
services.AddWork<ExampleWork, DefaultWorkOptions>();
ExampleWork
is our work type.DefaultWorkOptions
is the work's options type.
After that the ConfigureServices
method should look like that:
.ConfigureServices(services =>
{
services.AddWork<ExampleWork, DefaultWorkOptions>();
})
At this point our Worker Service is ready and we can already test it by running the project. You can notice, however, that the work iteration is only executed once. This is because we haven't configured the work options. By default, work iteration is only executed once. We can change that by configuring the options like so:
services.AddWork<ExampleWork, DefaultWorkOptions>(configure: opt =>
{
opt.DelayBetweenIterationsInSeconds = 10;
});
Now the second iteration will execute 10 seconds after the first iteration finishes.
TIP: You can use your own options classes by inheriting IWorkOptions
interface or even DefaultWorkOptions
type itself.
dotWork supports two ways to inject services into works. You are free to use any combination of these.
You can inject any singleton service into the work by adding a corresponding argument to the work's constructor, just like you would normally do:
public class ExampleWork : WorkBase<DefaultWorkOptions>
{
readonly SingletonService _singletonService;
public ExampleWork(SingletonService singletonService)
{
_singletonService = singletonService;
}
...
}
If you have any scoped services you would like to use with your works then you may inject them directly into the ExecuteIteration
method. A new scope and thus a new instance of a service will be created for each iteration.
public async Task ExecuteIteration(ScopedService scopedService, CancellationToken ct)
{
await Task.Delay(TimeSpan.FromSeconds(1), ct);
Console.WriteLine("Work iteration finished!");
}
It is possible to register all works at once by calling an AddWorks
extension method. It is mandatory to provide works configuration section. This configuration section must contain a dictionary of configuration sections for each work with the key value being a name of the work. For example, if we have two works:
ExampleWork1
ExampleWork2
then we should have an appSettings.json
file that looks something like that:
{
"Works": {
"ExampleWork1": {
"IsEnabled": false,
"DelayBetweenIterationsInSeconds": 86400 // 1 day
},
"ExampleWork2": {
"DelayBetweenIterationsInSeconds": 3600 // 1 hour
}
}
}
And the registration code should look somewhat like that:
.ConfigureServices((ctx, services) =>
{
var cfg = ctx.Configuration;
services.AddWorks(cfg.GetSection("Works"));
})
Important! Only works contained in an assembly that called this method are registered.
TIP: it is possible to override the specific work's registration by calling AddWork
on it after an AddWorks
call:
.ConfigureServices((ctx, services) =>
{
var cfg = ctx.Configuration;
services.AddWorks(cfg.GetSection("Works"));
services.AddWork<ExampleWork1, DefaultWorkOptions>(configure: opt =>
{
opt.DelayBetweenIterationsInSeconds = 86400 * 2; // 2 days
});
})
Also, you could specify assembly, that contains works:
.ConfigureServices((ctx, services) =>
{
var cfg = ctx.Configuration;
Assembly worksAssembly = ...;
services.AddWorksFromAssembly(cfg.GetSection("Works"), worksAssembly);
})
Or just provide a class from assembly, that contains works:
.ConfigureServices((ctx, services) =>
{
var cfg = ctx.Configuration;
services.AddWorksFromAssemblyContaining<MyAwesomeWork>(cfg.GetSection("Works"));
})