Skip to content

Commit

Permalink
Merge pull request #15 from atc-net/feature/BackgroundScheduleService…
Browse files Browse the repository at this point in the history
…Base

Add BackgroundScheduleServiceBase<T>
  • Loading branch information
davidkallesen authored Jul 20, 2024
2 parents 220d5af + 6747ac6 commit b0dbd1c
Show file tree
Hide file tree
Showing 14 changed files with 578 additions and 18 deletions.
4 changes: 2 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@
<ItemGroup Label="Code Analyzers">
<PackageReference Include="AsyncFixer" Version="1.6.0" PrivateAssets="All" />
<PackageReference Include="Asyncify" Version="0.9.7" PrivateAssets="All" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.146" PrivateAssets="All" />
<PackageReference Include="Meziantou.Analyzer" Version="2.0.161" PrivateAssets="All" />
<PackageReference Include="SecurityCodeScan.VS2019" Version="5.6.7" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435" PrivateAssets="All" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.23.1.88495" PrivateAssets="All" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.29.0.95321" PrivateAssets="All" />
</ItemGroup>

</Project>
162 changes: 155 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,39 @@ The Atc.Hosting namespace serves as a toolbox for building scalable and reliable

# BackgroundServiceBase`<T>`

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.
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.

This class is based on repeat intervals.

# BackgroundScheduleServiceBase`<T>`

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.
It extends the ASP.NET Core's `BackgroundService` class, providing a more robust framework for handling background tasks.

This class is based on cron expression for scheduling.

- More information about cron expressions can be found on [wiki](https://en.wikipedia.org/wiki/Cron)
- To get help with defining a cron expression, use this [cron online helper](https://crontab.cronhub.io/)

## Cron format

Cron expression is a mask to define fixed times, dates and intervals.
The mask consists of second (optional), minute, hour, day-of-month, month and day-of-week fields.
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.

```
Allowed values Allowed special characters Comment
┌───────────── second (optional) 0-59 * , - /
│ ┌───────────── minute 0-59 * , - /
│ │ ┌───────────── hour 0-23 * , - /
│ │ │ ┌───────────── day of month 1-31 * , - / L W ?
│ │ │ │ ┌───────────── month 1-12 or JAN-DEC * , - /
│ │ │ │ │ ┌───────────── day of week 0-6 or SUN-SAT * , - / # L ? Both 0 and 7 means SUN
│ │ │ │ │ │
* * * * * *
```

## Features

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

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

### Configuration Options

- Allows for startup delays.
- Configurable repeat interval for running tasks.
- Allows for `startup delays` for `BackgroundServiceBase`.
- Configurable `repeat interval` for running tasks with `BackgroundServiceBase`.
- Configurable `cron expression` for scheduling running tasks with `BackgroundScheduleServiceBase`.

### Ease of Use

Expand All @@ -74,6 +107,21 @@ public class MyBackgroundService : BackgroundServiceBase<MyBackgroundService>
}
```

```csharp
public class MyBackgroundService : BackgroundScheduleServiceBase<MyBackgroundService>
{
public MyBackgroundService(ILogger<MyBackgroundService> logger, IBackgroundScheduleServiceOptions options)
: base(logger, options)
{
}

public override Task DoWorkAsync(CancellationToken stoppingToken)
{
// Your background task logic here
}
}
```

## Setup BackgroundService via Dependency Injection

```csharp
Expand All @@ -92,13 +140,38 @@ var host = Host
})
.Build();

host.Run();
await host.RunAsync();
```

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

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

## Setup BackgroundScheduleServiceBase via Dependency Injection

```csharp
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.Build();

var host = Host
.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddSingleton<ITimeService, TimeService>();
services.Configure<TimeFileScheduleWorkerOptions>(configuration.GetSection(TimeFileScheduleWorkerOptions.SectionName));
services.AddHostedService<TimeFileScheduleWorker>();
})
.Build();

await host.RunAsync();
```

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

Note: `TimeFileScheduleWorker` uses `TimeFileScheduleWorkerOptions` that implements `IBackgroundScheduleServiceOptions`.

# BackgroundServiceHealthService

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

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

return File.AppendAllLinesAsync(
outFile,
contents: [$"{time:yyyy-MM-dd HH:mm:ss} - {ServiceName} - IsRunning={isServiceRunning}"],
stoppingToken);
}

protected override Task OnExceptionAsync(
Exception exception,
CancellationToken stoppingToken)
{
if (exception is IOException or UnauthorizedAccessException)
{
logger.LogCritical(exception, "Could not write file!");
return StopAsync(stoppingToken);
}

return base.OnExceptionAsync(exception, stoppingToken);
}
}
```

# Complete TimeFileScheduleWorker example

A sample reference implementation can be found in the sample project [`Atc.Hosting.TimeFile.Sample`](sample/Atc.Hosting.TimeFile.Sample/Program.cs)
which shows an example of the service `TimeFileScheduleWorker` that uses `BackgroundScheduleServiceBase` and the `IBackgroundServiceHealthService`.

```csharp
public class TimeFileScheduleWorker : BackgroundScheduleServiceBase<TimeFileScheduleWorker>
{
private readonly ITimeProvider timeProvider;

private readonly TimeFileScheduleWorkerOptions workerOptions;

public TimeFileWorker(
ILogger<TimeFileScheduleWorker> logger,
IBackgroundServiceHealthService healthService,
ITimeProvider timeProvider,
IOptions<TimeFileScheduleWorkerOptions> workerOptions)
: base(
logger,
workerOptions.Value,
healthService)
{
this.timeProvider = timeProvider;
this.workerOptions = workerOptions.Value;
}

public override Task StartAsync(
CancellationToken cancellationToken)
{
return base.StartAsync(cancellationToken);
}

public override Task StopAsync(
CancellationToken cancellationToken)
{
return base.StopAsync(cancellationToken);
}

public override Task DoWorkAsync(
CancellationToken stoppingToken)
{
var isServiceRunning = healthService.IsServiceRunning(nameof(TimeFileWorker));

Directory.CreateDirectory(workerOptions.OutputDirectory);

var time = timeProvider.UtcNow;

var outFile = Path.Combine(
workerOptions.OutputDirectory,
$"{nameof(TimeFileScheduleWorker)}.txt");

return File.WriteAllTextAsync(outFile, $"{ServiceName}-{isServiceRunning}", stoppingToken);
return File.AppendAllLinesAsync(
outFile,
contents: [$"{time:yyyy-MM-dd HH:mm:ss} - {ServiceName} - IsRunning={isServiceRunning}"],
stoppingToken);
}

protected override Task OnExceptionAsync(
Expand Down
6 changes: 5 additions & 1 deletion sample/Atc.Hosting.TimeFile.Sample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@
.ConfigureServices(services =>
{
services.AddSingleton<ITimeProvider, SystemTimeProvider>();
services.Configure<TimeFileWorkerOptions>(configuration.GetSection(TimeFileWorkerOptions.SectionName));
services.AddHostedService<TimeFileWorker>();
services.Configure<TimeFileScheduleWorkerOptions>(configuration.GetSection(TimeFileScheduleWorkerOptions.SectionName));
services.AddHostedService<TimeFileScheduleWorker>();
services.AddSingleton<IBackgroundServiceHealthService, BackgroundServiceHealthService>(s =>
{
var healthService = new BackgroundServiceHealthService(s.GetRequiredService<ITimeProvider>());
Expand All @@ -24,4 +28,4 @@
})
.Build();

host.Run();
await host.RunAsync();
66 changes: 66 additions & 0 deletions sample/Atc.Hosting.TimeFile.Sample/TimeFileScheduleWorker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
namespace Atc.Hosting.TimeFile.Sample;

public class TimeFileScheduleWorker : BackgroundScheduleServiceBase<TimeFileScheduleWorker>
{
private readonly ITimeProvider timeProvider;

private readonly TimeFileScheduleWorkerOptions workerOptions;

public TimeFileScheduleWorker(
ILogger<TimeFileScheduleWorker> logger,
IBackgroundServiceHealthService healthService,
ITimeProvider timeProvider,
IOptions<TimeFileScheduleWorkerOptions> workerOptions)
: base(
logger,
workerOptions.Value,
healthService)
{
this.timeProvider = timeProvider;
this.workerOptions = workerOptions.Value;
}

public override Task StartAsync(
CancellationToken cancellationToken)
{
return base.StartAsync(cancellationToken);
}

public override Task StopAsync(
CancellationToken cancellationToken)
{
return base.StopAsync(cancellationToken);
}

public override Task DoWorkAsync(
CancellationToken stoppingToken)
{
var isServiceRunning = healthService.IsServiceRunning(nameof(TimeFileWorker));

Directory.CreateDirectory(workerOptions.OutputDirectory);

var time = timeProvider.UtcNow;

var outFile = Path.Combine(
workerOptions.OutputDirectory,
$"{nameof(TimeFileWorker)}.txt");

return File.AppendAllLinesAsync(
outFile,
[$"{time:yyyy-MM-dd HH:mm:ss} - {ServiceName} - IsRunning={isServiceRunning}"],
stoppingToken);
}

protected override Task OnExceptionAsync(
Exception exception,
CancellationToken stoppingToken)
{
if (exception is IOException or UnauthorizedAccessException)
{
logger.LogCritical(exception, "Could not write file!");
return StopAsync(stoppingToken);
}

return base.OnExceptionAsync(exception, stoppingToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Atc.Hosting.TimeFile.Sample;

public class TimeFileScheduleWorkerOptions : IBackgroundScheduleServiceOptions
{
public const string SectionName = "TimeFileScheduleWorker";

public string OutputDirectory { get; set; } = Path.GetTempPath();

Check warning on line 7 in sample/Atc.Hosting.TimeFile.Sample/TimeFileScheduleWorkerOptions.cs

View workflow job for this annotation

GitHub Actions / merge-to-stable

Make sure publicly writable directories are used safely here. (https://rules.sonarsource.com/csharp/RSPEC-5443)

public string CronExpression { get; set; } = "*/5 * * * *";

public override string ToString()
=> $"{nameof(OutputDirectory)}: {OutputDirectory}, {nameof(CronExpression)}: {CronExpression}";
}
7 changes: 5 additions & 2 deletions sample/Atc.Hosting.TimeFile.Sample/TimeFileWorker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,12 @@ public override Task DoWorkAsync(

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

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

protected override Task OnExceptionAsync(
Expand Down
8 changes: 6 additions & 2 deletions sample/Atc.Hosting.TimeFile.Sample/appsettings.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
{
"TimeFileWorker": {
"OutputDirectory": "C:\\Temp\\TimeFileWorkerService",
"OutputDirectory": "C:\\Temp\\TimeFileWorkerTesting",
"StartupDelaySeconds": 1,
"RetryCount": 3,
"RepeatIntervalSeconds": 10
},
"TimeFileScheduleWorker": {
"OutputDirectory": "C:\\Temp\\TimeFileWorkerTesting",
"CronExpression": " */1 * * * *",
"RepeatIntervalSeconds": 10
}
}
7 changes: 6 additions & 1 deletion src/Atc.Hosting/Atc.Hosting.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,14 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Atc" Version="2.0.465" />
<PackageReference Include="Atc" Version="2.0.495" />
<PackageReference Include="Cronos" Version="0.8.4" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
</ItemGroup>

<ItemGroup>
<PackageReference Update="Nerdbank.GitVersioning" Version="3.6.139" />
</ItemGroup>

</Project>
Loading

0 comments on commit b0dbd1c

Please sign in to comment.