This package can be used to easily define and run time-triggered background jobs.
$ dotnet add package devdeer.Libraries.JobScheduler
If you want to use this package you should be aware of some principles and practices we at DEVDEER use. So be aware that this is not backed by a public repository. The purpose here is to give out customers easy access to our logic. Nevertheless you are welcome to use this lib if it provides any advantages.
This package can be used to easily configure a bunch of jobs which should be started based on configurable CRON triggers. THis packages uses Quartz.NET to implement triggers.
The default scenario targetted by this library is to be used by console applications which act as a long-running background service. The idea is that you can run a single or multiple jobs easily just by implementing an interface and add some configuration.
Create a new console project and install the Nuget package:
dotnet new console -n Sample
cd Sample
dotnet add package devdeer.Libraries.JobScheduler --prerelease
Open the project in your favorite editor and replace the code of Program.cs with:
using devdeer.Libraries.JobScheduler;
using devdeer.Libraries.JobScheduler.Attributes;
using devdeer.Libraries.JobScheduler.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Quartz;
var builder = Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.UseScheduler(
hostContext,
opt =>
{
// TODO configure options and callbacks here
});
});
var app = builder.Build();
await app.StartJobsAsync();
[Job("Sample")]
public class SampleJob : BaseBackgroundJob
{
/// <inheritdoc />
public SampleJob(ILogger<SampleJob> logger) : base(logger)
{
}
/// <inheritdoc />
protected override async Task<bool> InternalExecuteAsync(IJobExecutionContext context)
{
Logger.LogInformation("I'M RUNNING");
await Task.Yield();
return true;
}
}
Add a file to the project-directory named appsettings.json and give it the following content:
{
"JobScheduler": {
"DisplayTimeZoneId": "W. Europe Standard Time",
"Jobs": {
"Sample": {
"JobName": "Sample Job",
"Schedule": "*/5 * * * * ?",
"Enabled": true,
"Namespace": "Demo",
"RunImmediately": true
}
}
}
}
Open the file Sample.csproj in an editor and add the following section:
<ItemGroup>
<None Remove="appsettings.json" />
<Content Include="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
The complete Sample.csproj should look like this:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\stage-3\JobScheduler\JobScheduler.csproj"/>
</ItemGroup>
<ItemGroup>
<None Remove="appsettings.json" />
<Content Include="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>
Now go back to your terminal and execute
dotnet run
You should see a bunch of info-loggings. At the end you should see a new log entry every 5 seconds.
The central idea is that you will provide classes which implement the interface IBackgroundJob. If you want
to inherit more conveniently just inherit from BaseBackgroundJob. This way your will have a logger hooked up
and you just override one method. One important thing is that you need to decorate your class with the
JobAttribute:
[Jobs("MyKey")]
public class MyJob : BaseBackgroundJob
{
public SampleJob(ILogger<SampleJob> logger) : base(logger)
{
}
protected override async Task<bool> InternalExecuteAsync(IJobExecutionContext context)
{
Logger.LogInformation("Started");
// TODO your code here
return Task.FromResult(true);
}
}
InternalExecuteAsync should return false if some internal error occured. This signals the
control plane that this job run was not finished successfully.
After you have implemented this the next step is to add some configuration data. You can use appsettings.json,
environment variables, user secrets or Azure App Configuration. The internal implementation of the settings binder
is using IOptionsSnapshot<> so that reloadable config stores are fully supported.
The simplest solution is to use the appsettings.json:
{
"JobScheduler": {
"DisplayTimeZoneId": "W. Europe Standard Time",
"Jobs": {
"MyJob": {
"JobName": "Just a sample",
"Schedule": "*/5 * * * * ?",
"Enabled": true,
"Namespace": "Demo",
"RunImmediately": true
}
}
}
}
The default key for configurations is the shown JobScheduler key. You can change this at will (see below). The
DisplayTimeZoneId is used to provide a TimeZone-identifier so that the tooling understands and logs times related
to that zone. The default time zone is UTC.
The second option Jobs is a dictionary. The key is a string which must match the key of the job
that you defined in the attribute in the first step. The options provided are:
| Option | Mandatory | Description |
|---|---|---|
| JobName | x | A human readable name mainly used for logging. |
| Namespace | x | A string which can be used to put jobs together in groups. |
| Schedule | x | A unix CRONTAB expression to define the rhythm with which to call the job. |
| Enabled | A boolean value to switch this job on or off. Defaults to true. | |
| RunImmediately | A boolean value which indicates if this job should run as soon as the app starts. |
You can add as many jobs as you like by just adding another class and then add a matching element
to the Jobs-dictionary.
The final step is to call 2 extensions-methods in your app startup. The samples assume that your are familiar with the concept of .NET app hosting. If you struggle with this code you might want to read our blog article about Hosted Console Apps first.
However the following code is all you need to add:
var builder = Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.UseScheduler(hostContext);
});
var app = builder.Build();
await app.StartJobsAsync();
This represents the minimal code. There are 2 more overloads.
If you want to configure some specific settings use:
var builder = Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.UseScheduler(hostContext, opt =>
{
});
});
The options variable passed in (opt in the sample above) contains some properties:
| Option | Description |
|---|---|
| AfterJobFailAsync | A callback that will be triggered after a job failed (i.e. the method returned false. |
| AfterJobSuccessAsync | A callback that will be triggered after a job succeeded (i.e. the method returned true. |
| BeforeJobRunAsync | A callback that will be triggered before a single job is executed. |
| FailOnJobError | A boolean which if set to true (default is false) will let the whole process die if a job does not return true. |
| JobTriggers | The data loaded from the .NET configuration. |
| OnJobException | A callback that will be triggered if a job exception occured providing details about it. |
| QuartzOptions | Gives direct access to the detailled options of the Quartz configuration. |
| ThrowExceptions | A boolean which controls if exceptions from the control plane / jobs are rethrown to you. |
If you want to store your settings under a key which differs from the default JobScheduler
you can do this with:
BeforeJobRunAsync is a good place to run configuration reloads (for instance if you use Azure App Configuration). You could initialize
config reloads like this:
var builder = Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.UseScheduler(
hostContext,
opt =>
{
opt.BeforeJobRunAsync = async (provider, context) =>
{
var refresher = provider.GetService<IConfigurationRefresher>();
if (refresher == null)
{
throw new InvalidOperationException("Could not retrieve configuration refresher service.");
}
await refresher.TryRefreshAsync();
};
});
});
DEVDEER is a company from Magdeburg, Germany which is specialized in consulting, project management and development. We are very focussed on Microsoft Azure as a platform to run our products and solutions in the cloud. So all of our backend components usually runs in this environment and is developed using .NET. In the frontend area we are using react and a huge bunch of packages to build our UIs.
You can find us online: