Skip to content

Commit b0dbd1c

Browse files
Merge pull request #15 from atc-net/feature/BackgroundScheduleServiceBase
Add BackgroundScheduleServiceBase<T>
2 parents 220d5af + 6747ac6 commit b0dbd1c

14 files changed

+578
-18
lines changed

Directory.Build.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@
4343
<ItemGroup Label="Code Analyzers">
4444
<PackageReference Include="AsyncFixer" Version="1.6.0" PrivateAssets="All" />
4545
<PackageReference Include="Asyncify" Version="0.9.7" PrivateAssets="All" />
46-
<PackageReference Include="Meziantou.Analyzer" Version="2.0.146" PrivateAssets="All" />
46+
<PackageReference Include="Meziantou.Analyzer" Version="2.0.161" PrivateAssets="All" />
4747
<PackageReference Include="SecurityCodeScan.VS2019" Version="5.6.7" PrivateAssets="All" />
4848
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
49-
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.23.1.88495" PrivateAssets="All" />
49+
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.29.0.95321" PrivateAssets="All" />
5050
</ItemGroup>
5151

5252
</Project>

README.md

Lines changed: 155 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,39 @@ The Atc.Hosting namespace serves as a toolbox for building scalable and reliable
3232

3333
# BackgroundServiceBase`<T>`
3434

35-
The `BackgroundServiceBase<T>` class serves as a base for continuous long running background services that require enhanced features like custom logging and configurable service options. It extends the ASP.NET Core's `BackgroundService` class, providing a more robust framework for handling background tasks.
35+
The `BackgroundServiceBase<T>` class serves as a base for continuous long running background services that require enhanced features like custom logging and configurable service options.
36+
It extends the ASP.NET Core's `BackgroundService` class, providing a more robust framework for handling background tasks.
37+
38+
This class is based on repeat intervals.
39+
40+
# BackgroundScheduleServiceBase`<T>`
41+
42+
The `BackgroundScheduleServiceBase<T>` class serves as a base for continuous long running background services that require enhanced features like custom logging and configurable service options.
43+
It extends the ASP.NET Core's `BackgroundService` class, providing a more robust framework for handling background tasks.
44+
45+
This class is based on cron expression for scheduling.
46+
47+
- More information about cron expressions can be found on [wiki](https://en.wikipedia.org/wiki/Cron)
48+
- To get help with defining a cron expression, use this [cron online helper](https://crontab.cronhub.io/)
49+
50+
## Cron format
51+
52+
Cron expression is a mask to define fixed times, dates and intervals.
53+
The mask consists of second (optional), minute, hour, day-of-month, month and day-of-week fields.
54+
All of the fields allow you to specify multiple values, and any given date/time will satisfy the specified Cron expression, if all the fields contain a matching value.
55+
56+
```
57+
Allowed values Allowed special characters Comment
58+
59+
┌───────────── second (optional) 0-59 * , - /
60+
│ ┌───────────── minute 0-59 * , - /
61+
│ │ ┌───────────── hour 0-23 * , - /
62+
│ │ │ ┌───────────── day of month 1-31 * , - / L W ?
63+
│ │ │ │ ┌───────────── month 1-12 or JAN-DEC * , - /
64+
│ │ │ │ │ ┌───────────── day of week 0-6 or SUN-SAT * , - / # L ? Both 0 and 7 means SUN
65+
│ │ │ │ │ │
66+
* * * * * *
67+
```
3668

3769
## Features
3870

@@ -44,14 +76,15 @@ The `BackgroundServiceBase<T>` class serves as a base for continuous long runnin
4476
### Error Handling
4577

4678
- Catches unhandled exceptions and logs them with a severity of `LogLevel.Warning`.
47-
- Reruns the `DoWorkAsync` method after a configurable repeat interval.
79+
- Reruns the `DoWorkAsync` method after a configurable `repeat interval` for `BackgroundServiceBase` or `scheduled` for `BackgroundScheduleServiceBase`.
4880
- For manual error handling hook into the exception handling in `DoWorkAsync` by overriding the `OnExceptionAsync` method.
4981
- Designed to log errors rather than crashing the service.
5082

5183
### Configuration Options
5284

53-
- Allows for startup delays.
54-
- Configurable repeat interval for running tasks.
85+
- Allows for `startup delays` for `BackgroundServiceBase`.
86+
- Configurable `repeat interval` for running tasks with `BackgroundServiceBase`.
87+
- Configurable `cron expression` for scheduling running tasks with `BackgroundScheduleServiceBase`.
5588

5689
### Ease of Use
5790

@@ -74,6 +107,21 @@ public class MyBackgroundService : BackgroundServiceBase<MyBackgroundService>
74107
}
75108
```
76109

110+
```csharp
111+
public class MyBackgroundService : BackgroundScheduleServiceBase<MyBackgroundService>
112+
{
113+
public MyBackgroundService(ILogger<MyBackgroundService> logger, IBackgroundScheduleServiceOptions options)
114+
: base(logger, options)
115+
{
116+
}
117+
118+
public override Task DoWorkAsync(CancellationToken stoppingToken)
119+
{
120+
// Your background task logic here
121+
}
122+
}
123+
```
124+
77125
## Setup BackgroundService via Dependency Injection
78126

79127
```csharp
@@ -92,13 +140,38 @@ var host = Host
92140
})
93141
.Build();
94142

95-
host.Run();
143+
await host.RunAsync();
96144
```
97145

98146
In this example the `TimeFileWorker` BackgroundService is wired up by using `AddHostedService<T>` as a normal `BackgroundService`.
99147

100148
Note: `TimeFileWorker` uses `TimeFileWorkerOptions` that implements `IBackgroundServiceOptions`.
101149

150+
## Setup BackgroundScheduleServiceBase via Dependency Injection
151+
152+
```csharp
153+
var configuration = new ConfigurationBuilder()
154+
.SetBasePath(Directory.GetCurrentDirectory())
155+
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
156+
.Build();
157+
158+
var host = Host
159+
.CreateDefaultBuilder(args)
160+
.ConfigureServices(services =>
161+
{
162+
services.AddSingleton<ITimeService, TimeService>();
163+
services.Configure<TimeFileScheduleWorkerOptions>(configuration.GetSection(TimeFileScheduleWorkerOptions.SectionName));
164+
services.AddHostedService<TimeFileScheduleWorker>();
165+
})
166+
.Build();
167+
168+
await host.RunAsync();
169+
```
170+
171+
In this example the `TimeFileScheduleWorker` BackgroundService is wired up by using `AddHostedService<T>` as a normal `BackgroundService`.
172+
173+
Note: `TimeFileScheduleWorker` uses `TimeFileScheduleWorkerOptions` that implements `IBackgroundScheduleServiceOptions`.
174+
102175
# BackgroundServiceHealthService
103176

104177
`IBackgroundServiceHealthService` is an interface that provides methods to manage and monitor the health of background services in a .NET application.
@@ -245,9 +318,84 @@ public class TimeFileWorker : BackgroundServiceBase<TimeFileWorker>
245318

246319
var outFile = Path.Combine(
247320
workerOptions.OutputDirectory,
248-
$"{time:yyyy-MM-dd--HHmmss}-{isServiceRunning}.txt");
321+
$"{nameof(TimeFileWorker)}.txt");
322+
323+
return File.AppendAllLinesAsync(
324+
outFile,
325+
contents: [$"{time:yyyy-MM-dd HH:mm:ss} - {ServiceName} - IsRunning={isServiceRunning}"],
326+
stoppingToken);
327+
}
328+
329+
protected override Task OnExceptionAsync(
330+
Exception exception,
331+
CancellationToken stoppingToken)
332+
{
333+
if (exception is IOException or UnauthorizedAccessException)
334+
{
335+
logger.LogCritical(exception, "Could not write file!");
336+
return StopAsync(stoppingToken);
337+
}
338+
339+
return base.OnExceptionAsync(exception, stoppingToken);
340+
}
341+
}
342+
```
343+
344+
# Complete TimeFileScheduleWorker example
345+
346+
A sample reference implementation can be found in the sample project [`Atc.Hosting.TimeFile.Sample`](sample/Atc.Hosting.TimeFile.Sample/Program.cs)
347+
which shows an example of the service `TimeFileScheduleWorker` that uses `BackgroundScheduleServiceBase` and the `IBackgroundServiceHealthService`.
348+
349+
```csharp
350+
public class TimeFileScheduleWorker : BackgroundScheduleServiceBase<TimeFileScheduleWorker>
351+
{
352+
private readonly ITimeProvider timeProvider;
353+
354+
private readonly TimeFileScheduleWorkerOptions workerOptions;
355+
356+
public TimeFileWorker(
357+
ILogger<TimeFileScheduleWorker> logger,
358+
IBackgroundServiceHealthService healthService,
359+
ITimeProvider timeProvider,
360+
IOptions<TimeFileScheduleWorkerOptions> workerOptions)
361+
: base(
362+
logger,
363+
workerOptions.Value,
364+
healthService)
365+
{
366+
this.timeProvider = timeProvider;
367+
this.workerOptions = workerOptions.Value;
368+
}
369+
370+
public override Task StartAsync(
371+
CancellationToken cancellationToken)
372+
{
373+
return base.StartAsync(cancellationToken);
374+
}
375+
376+
public override Task StopAsync(
377+
CancellationToken cancellationToken)
378+
{
379+
return base.StopAsync(cancellationToken);
380+
}
381+
382+
public override Task DoWorkAsync(
383+
CancellationToken stoppingToken)
384+
{
385+
var isServiceRunning = healthService.IsServiceRunning(nameof(TimeFileWorker));
386+
387+
Directory.CreateDirectory(workerOptions.OutputDirectory);
388+
389+
var time = timeProvider.UtcNow;
390+
391+
var outFile = Path.Combine(
392+
workerOptions.OutputDirectory,
393+
$"{nameof(TimeFileScheduleWorker)}.txt");
249394

250-
return File.WriteAllTextAsync(outFile, $"{ServiceName}-{isServiceRunning}", stoppingToken);
395+
return File.AppendAllLinesAsync(
396+
outFile,
397+
contents: [$"{time:yyyy-MM-dd HH:mm:ss} - {ServiceName} - IsRunning={isServiceRunning}"],
398+
stoppingToken);
251399
}
252400

253401
protected override Task OnExceptionAsync(

sample/Atc.Hosting.TimeFile.Sample/Program.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@
99
.ConfigureServices(services =>
1010
{
1111
services.AddSingleton<ITimeProvider, SystemTimeProvider>();
12+
1213
services.Configure<TimeFileWorkerOptions>(configuration.GetSection(TimeFileWorkerOptions.SectionName));
1314
services.AddHostedService<TimeFileWorker>();
1415

16+
services.Configure<TimeFileScheduleWorkerOptions>(configuration.GetSection(TimeFileScheduleWorkerOptions.SectionName));
17+
services.AddHostedService<TimeFileScheduleWorker>();
18+
1519
services.AddSingleton<IBackgroundServiceHealthService, BackgroundServiceHealthService>(s =>
1620
{
1721
var healthService = new BackgroundServiceHealthService(s.GetRequiredService<ITimeProvider>());
@@ -24,4 +28,4 @@
2428
})
2529
.Build();
2630

27-
host.Run();
31+
await host.RunAsync();
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
namespace Atc.Hosting.TimeFile.Sample;
2+
3+
public class TimeFileScheduleWorker : BackgroundScheduleServiceBase<TimeFileScheduleWorker>
4+
{
5+
private readonly ITimeProvider timeProvider;
6+
7+
private readonly TimeFileScheduleWorkerOptions workerOptions;
8+
9+
public TimeFileScheduleWorker(
10+
ILogger<TimeFileScheduleWorker> logger,
11+
IBackgroundServiceHealthService healthService,
12+
ITimeProvider timeProvider,
13+
IOptions<TimeFileScheduleWorkerOptions> workerOptions)
14+
: base(
15+
logger,
16+
workerOptions.Value,
17+
healthService)
18+
{
19+
this.timeProvider = timeProvider;
20+
this.workerOptions = workerOptions.Value;
21+
}
22+
23+
public override Task StartAsync(
24+
CancellationToken cancellationToken)
25+
{
26+
return base.StartAsync(cancellationToken);
27+
}
28+
29+
public override Task StopAsync(
30+
CancellationToken cancellationToken)
31+
{
32+
return base.StopAsync(cancellationToken);
33+
}
34+
35+
public override Task DoWorkAsync(
36+
CancellationToken stoppingToken)
37+
{
38+
var isServiceRunning = healthService.IsServiceRunning(nameof(TimeFileWorker));
39+
40+
Directory.CreateDirectory(workerOptions.OutputDirectory);
41+
42+
var time = timeProvider.UtcNow;
43+
44+
var outFile = Path.Combine(
45+
workerOptions.OutputDirectory,
46+
$"{nameof(TimeFileWorker)}.txt");
47+
48+
return File.AppendAllLinesAsync(
49+
outFile,
50+
[$"{time:yyyy-MM-dd HH:mm:ss} - {ServiceName} - IsRunning={isServiceRunning}"],
51+
stoppingToken);
52+
}
53+
54+
protected override Task OnExceptionAsync(
55+
Exception exception,
56+
CancellationToken stoppingToken)
57+
{
58+
if (exception is IOException or UnauthorizedAccessException)
59+
{
60+
logger.LogCritical(exception, "Could not write file!");
61+
return StopAsync(stoppingToken);
62+
}
63+
64+
return base.OnExceptionAsync(exception, stoppingToken);
65+
}
66+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace Atc.Hosting.TimeFile.Sample;
2+
3+
public class TimeFileScheduleWorkerOptions : IBackgroundScheduleServiceOptions
4+
{
5+
public const string SectionName = "TimeFileScheduleWorker";
6+
7+
public string OutputDirectory { get; set; } = Path.GetTempPath();
8+
9+
public string CronExpression { get; set; } = "*/5 * * * *";
10+
11+
public override string ToString()
12+
=> $"{nameof(OutputDirectory)}: {OutputDirectory}, {nameof(CronExpression)}: {CronExpression}";
13+
}

sample/Atc.Hosting.TimeFile.Sample/TimeFileWorker.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,12 @@ public override Task DoWorkAsync(
4343

4444
var outFile = Path.Combine(
4545
workerOptions.OutputDirectory,
46-
$"{time:yyyy-MM-dd--HHmmss}-{isServiceRunning}.txt");
46+
$"{nameof(TimeFileWorker)}.txt");
4747

48-
return File.WriteAllTextAsync(outFile, $"{ServiceName}-{isServiceRunning}", stoppingToken);
48+
return File.AppendAllLinesAsync(
49+
outFile,
50+
[$"{time:yyyy-MM-dd HH:mm:ss} - {ServiceName} - IsRunning={isServiceRunning}"],
51+
stoppingToken);
4952
}
5053

5154
protected override Task OnExceptionAsync(
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
{
22
"TimeFileWorker": {
3-
"OutputDirectory": "C:\\Temp\\TimeFileWorkerService",
3+
"OutputDirectory": "C:\\Temp\\TimeFileWorkerTesting",
44
"StartupDelaySeconds": 1,
5-
"RetryCount": 3,
5+
"RepeatIntervalSeconds": 10
6+
},
7+
"TimeFileScheduleWorker": {
8+
"OutputDirectory": "C:\\Temp\\TimeFileWorkerTesting",
9+
"CronExpression": " */1 * * * *",
610
"RepeatIntervalSeconds": 10
711
}
812
}

src/Atc.Hosting/Atc.Hosting.csproj

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,14 @@
1414
</ItemGroup>
1515

1616
<ItemGroup>
17-
<PackageReference Include="Atc" Version="2.0.465" />
17+
<PackageReference Include="Atc" Version="2.0.495" />
18+
<PackageReference Include="Cronos" Version="0.8.4" />
1819
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
1920
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
2021
</ItemGroup>
2122

23+
<ItemGroup>
24+
<PackageReference Update="Nerdbank.GitVersioning" Version="3.6.139" />
25+
</ItemGroup>
26+
2227
</Project>

0 commit comments

Comments
 (0)