Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add BackgroundScheduleServiceBase<T> #15

Merged
merged 4 commits into from
Jul 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();

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