Skip to content

Commit 9f40a1d

Browse files
committed
Added CRonBackgroundWorker example
1 parent 8d12917 commit 9f40a1d

11 files changed

+252
-0
lines changed
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using CronBackgroundWorker.Cron;
2+
3+
namespace CronBackgroundWorker;
4+
5+
public class AnotherCronJob : ICronJob
6+
{
7+
public Task Run(CancellationToken token = default)
8+
{
9+
Console.WriteLine($"Hello from {nameof(AnotherCronJob)} at: {DateTime.UtcNow.ToShortTimeString()}");
10+
11+
return Task.CompletedTask;
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using Microsoft.Extensions.DependencyInjection.Extensions;
2+
using NCrontab;
3+
4+
namespace CronBackgroundWorker.Cron;
5+
6+
public static class CronJobExtensions
7+
{
8+
public static IServiceCollection AddCronJob<T>(this IServiceCollection services, string cronExpression)
9+
where T : class, ICronJob
10+
{
11+
var cron = CrontabSchedule.TryParse(cronExpression)
12+
?? throw new ArgumentException("Invalid cron expression", nameof(cronExpression));
13+
14+
var entry = new CronRegistryEntry(typeof(T), cron);
15+
16+
// AddHostedService internally only registers one time
17+
services.AddHostedService<CronScheduler>();
18+
19+
// TryAdd prevents multiple registrations of T
20+
services.TryAddSingleton<T>();
21+
services.AddSingleton(entry);
22+
23+
return services;
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
using NCrontab;
2+
3+
namespace CronBackgroundWorker.Cron;
4+
5+
public sealed record CronRegistryEntry(Type Type, CrontabSchedule CrontabSchedule);
+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
namespace CronBackgroundWorker.Cron;
2+
3+
public sealed class CronScheduler : BackgroundService
4+
{
5+
private readonly IServiceProvider _serviceProvider;
6+
private readonly IReadOnlyCollection<CronRegistryEntry> _cronJobs;
7+
8+
public CronScheduler(
9+
IServiceProvider serviceProvider,
10+
IEnumerable<CronRegistryEntry> cronJobs)
11+
{
12+
// Use the container
13+
_serviceProvider = serviceProvider;
14+
_cronJobs = cronJobs.ToList();
15+
}
16+
17+
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
18+
{
19+
// Create a timer that has a resolution less than 60 seconds
20+
// Because cron has a resolution of a minute
21+
// So everything under will work
22+
using var tickTimer = new PeriodicTimer(TimeSpan.FromSeconds(30));
23+
24+
// Create a map of the next upcoming entries
25+
var runMap = new Dictionary<DateTime, List<Type>>();
26+
while (await tickTimer.WaitForNextTickAsync(stoppingToken))
27+
{
28+
// Get UTC Now with minute resolution (remove microseconds and seconds)
29+
var now = UtcNowMinutePrecision();
30+
31+
// Run jobs that are in the map
32+
RunActiveJobs(runMap, now, stoppingToken);
33+
34+
// Get the next run for the upcoming tick
35+
runMap = GetJobRuns();
36+
}
37+
}
38+
39+
private void RunActiveJobs(IReadOnlyDictionary<DateTime, List<Type>> runMap, DateTime now, CancellationToken stoppingToken)
40+
{
41+
if (!runMap.TryGetValue(now, out var currentRuns))
42+
{
43+
return;
44+
}
45+
46+
foreach (var run in currentRuns)
47+
{
48+
// We are sure (thanks to our extension method)
49+
// that the service is of type ICronJob
50+
var job = (ICronJob)_serviceProvider.GetRequiredService(run);
51+
52+
// We don't want to await jobs explicitly because that
53+
// could interfere with other job runs
54+
job.Run(stoppingToken);
55+
}
56+
}
57+
58+
private Dictionary<DateTime, List<Type>> GetJobRuns()
59+
{
60+
var runMap = new Dictionary<DateTime, List<Type>>();
61+
foreach (var cron in _cronJobs)
62+
{
63+
var utcNow = DateTime.UtcNow;
64+
var runDates = cron.CrontabSchedule.GetNextOccurrences(utcNow, utcNow.AddMinutes(1));
65+
if (runDates is not null)
66+
{
67+
AddJobRuns(runMap, runDates, cron);
68+
}
69+
}
70+
71+
return runMap;
72+
}
73+
74+
private static void AddJobRuns(IDictionary<DateTime, List<Type>> runMap, IEnumerable<DateTime> runDates, CronRegistryEntry cron)
75+
{
76+
foreach (var runDate in runDates)
77+
{
78+
if (runMap.TryGetValue(runDate, out var value))
79+
{
80+
value.Add(cron.Type);
81+
}
82+
else
83+
{
84+
runMap[runDate] = new List<Type> { cron.Type };
85+
}
86+
}
87+
}
88+
89+
private static DateTime UtcNowMinutePrecision()
90+
{
91+
var now = DateTime.UtcNow;
92+
return new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, 0);
93+
}
94+
}

CronBackgroundWorker/Cron/ICronJob.cs

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace CronBackgroundWorker.Cron;
2+
3+
public interface ICronJob
4+
{
5+
Task Run(CancellationToken token = default);
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net7.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="NCrontab" Version="3.3.1" />
11+
</ItemGroup>
12+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CronBackgroundWorker", "CronBackgroundWorker.csproj", "{DDB36A76-DBB7-47DC-8702-F4F36019193E}"
4+
EndProject
5+
Global
6+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
7+
Debug|Any CPU = Debug|Any CPU
8+
Release|Any CPU = Release|Any CPU
9+
EndGlobalSection
10+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
11+
{DDB36A76-DBB7-47DC-8702-F4F36019193E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
12+
{DDB36A76-DBB7-47DC-8702-F4F36019193E}.Debug|Any CPU.Build.0 = Debug|Any CPU
13+
{DDB36A76-DBB7-47DC-8702-F4F36019193E}.Release|Any CPU.ActiveCfg = Release|Any CPU
14+
{DDB36A76-DBB7-47DC-8702-F4F36019193E}.Release|Any CPU.Build.0 = Release|Any CPU
15+
EndGlobalSection
16+
EndGlobal

CronBackgroundWorker/CronJob.cs

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using CronBackgroundWorker.Cron;
2+
3+
namespace CronBackgroundWorker;
4+
5+
public class CronJob : ICronJob
6+
{
7+
private readonly ILogger<CronJob> _logger;
8+
9+
public CronJob(ILogger<CronJob> logger)
10+
{
11+
_logger = logger;
12+
}
13+
public Task Run(CancellationToken token = default)
14+
{
15+
_logger.LogInformation("Hello from {name} at: {time}", nameof(CronJob), DateTime.UtcNow.ToShortTimeString());
16+
17+
return Task.CompletedTask;
18+
}
19+
}

CronBackgroundWorker/Program.cs

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using CronBackgroundWorker;
2+
using CronBackgroundWorker.Cron;
3+
4+
var builder = WebApplication.CreateBuilder(args);
5+
6+
7+
builder.Services.AddCronJob<CronJob>("* * * * *");
8+
builder.Services.AddCronJob<AnotherCronJob>("*/2 * * * *");
9+
10+
var app = builder.Build();
11+
12+
app.Run();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"$schema": "https://json.schemastore.org/launchsettings.json",
3+
"iisSettings": {
4+
"windowsAuthentication": false,
5+
"anonymousAuthentication": true,
6+
"iisExpress": {
7+
"applicationUrl": "http://localhost:32380",
8+
"sslPort": 44343
9+
}
10+
},
11+
"profiles": {
12+
"http": {
13+
"commandName": "Project",
14+
"dotnetRunMessages": true,
15+
"launchBrowser": true,
16+
"launchUrl": "swagger",
17+
"applicationUrl": "http://localhost:5163",
18+
"environmentVariables": {
19+
"ASPNETCORE_ENVIRONMENT": "Development"
20+
}
21+
},
22+
"https": {
23+
"commandName": "Project",
24+
"dotnetRunMessages": true,
25+
"launchBrowser": true,
26+
"launchUrl": "swagger",
27+
"applicationUrl": "https://localhost:7250;http://localhost:5163",
28+
"environmentVariables": {
29+
"ASPNETCORE_ENVIRONMENT": "Development"
30+
}
31+
},
32+
"IIS Express": {
33+
"commandName": "IISExpress",
34+
"launchBrowser": true,
35+
"launchUrl": "swagger",
36+
"environmentVariables": {
37+
"ASPNETCORE_ENVIRONMENT": "Development"
38+
}
39+
}
40+
}
41+
}

CronBackgroundWorker/appsettings.json

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.AspNetCore": "Warning"
6+
}
7+
},
8+
"AllowedHosts": "*"
9+
}

0 commit comments

Comments
 (0)