Skip to content

Commit d479175

Browse files
Merge pull request #10 from Anders-Toegersen/feature/backgroundservicebase-overhaul
Feature: BackgroundServiceBase & BackgroundServiceHealthService Overhaul
2 parents 45b4ec0 + 8671fa1 commit d479175

21 files changed

+551
-164
lines changed

README.md

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# Atc.Hosting
44

5-
The Atc.Hosting namespace serves as a toolbox for building scalable and reliable hosting solutions, with an emphasis on background services. It contains classes and extension methods designed to handle common hosting scenarios, providing enhanced features like custom logging, retries, and advanced configuration options. The namespace aims to streamline development efforts and improve the maintainability of hosted applications.
5+
The Atc.Hosting namespace serves as a toolbox for building scalable and reliable hosting solutions, with an emphasis on background services. It contains classes and extension methods designed to handle common hosting scenarios, providing enhanced features like custom logging, and advanced configuration options. The namespace aims to streamline development efforts and improve the maintainability of hosted applications.
66

77
# Table of Contents
88

@@ -11,7 +11,6 @@ The Atc.Hosting namespace serves as a toolbox for building scalable and reliable
1111
- [BackgroundServiceBase`<T>`](#backgroundservicebaset)
1212
- [Features](#features)
1313
- [Logging](#logging)
14-
- [Retry Mechanism](#retry-mechanism)
1514
- [Error Handling](#error-handling)
1615
- [Configuration Options](#configuration-options)
1716
- [Ease of Use](#ease-of-use)
@@ -32,8 +31,7 @@ The Atc.Hosting namespace serves as a toolbox for building scalable and reliable
3231

3332
# BackgroundServiceBase`<T>`
3433

35-
The `BackgroundServiceBase<T>` class serves as a base for background services that require enhanced features like custom logging, retries, and configurable service options. It extends the ASP.NET Core's `BackgroundService` class, providing a more robust framework for handling
36-
background tasks.
34+
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.
3735

3836
## Features
3937

@@ -42,20 +40,16 @@ background tasks.
4240
- Utilizes `ILogger<T>` for type-specific, high-performance logging.
4341
- Automatically enriches log entries with the name of the service type (`T`).
4442

45-
### Retry Mechanism
46-
47-
- Built-in retries using the `Polly` library.
48-
- Retry count and exponential backoff settings are configurable.
49-
5043
### Error Handling
5144

52-
- Catches exceptions and logs them with a severity of `LogLevel.Warning`.
45+
- Catches unhandled exceptions and logs them with a severity of `LogLevel.Warning`.
46+
- Reruns the `DoWorkAsync` method after a configurable repeat interval.
47+
- For manual error handling hook into the exception handling in `DoWorkAsync` by overriding the `OnExceptionAsync` method.
5348
- Designed to log errors rather than crashing the service.
5449

5550
### Configuration Options
5651

5752
- Allows for startup delays.
58-
- Configurable retry count.
5953
- Configurable repeat interval for running tasks.
6054

6155
### Ease of Use
@@ -184,6 +178,23 @@ public override async Task DoWorkAsync(CancellationToken stoppingToken)
184178
}
185179
```
186180

181+
## Default implementation for `BackgroundServiceBase<>`
182+
The `BackgroundServiceBase<>` automatically uses the `IBackgroundServiceHealthService` in the `DoWorkAsync` 'wait and retry' loop. This is archieved by providing the base constructor with a `IBackgroundServiceHealthService` instance.
183+
184+
```csharp
185+
public TimeFileWorker(
186+
//...other parameters
187+
IBackgroundServiceHealthService healthService,
188+
//...other parameters
189+
)
190+
: base (healthService)
191+
{
192+
//...other initializations
193+
}
194+
```
195+
196+
Now you not have to set the running state of the service in the `BackgroundService.StartAsync` and `BackgroundService.StopAsync` methods.
197+
187198
# Complete TimeFileWorker example
188199

189200
A sample reference implementation can be found in the sample project [`Atc.Hosting.TimeFile.Sample`](sample/Atc.Hosting.TimeFile.Sample/Program.cs)
@@ -192,7 +203,6 @@ which shows an example of the service `TimeFileWorker` that uses `BackgroundServ
192203
```csharp
193204
public class TimeFileWorker : BackgroundServiceBase<TimeFileWorker>
194205
{
195-
private readonly IBackgroundServiceHealthService healthService;
196206
private readonly ITimeProvider timeProvider;
197207

198208
private readonly TimeFileWorkerOptions workerOptions;
@@ -204,28 +214,26 @@ public class TimeFileWorker : BackgroundServiceBase<TimeFileWorker>
204214
IOptions<TimeFileWorkerOptions> workerOptions)
205215
: base(
206216
logger,
207-
workerOptions.Value)
217+
workerOptions.Value,
218+
healthService)
208219
{
209-
this.healthService = healthService;
210220
this.timeProvider = timeProvider;
211221
this.workerOptions = workerOptions.Value;
212222
}
213223

214-
public override async Task StartAsync(
224+
public override Task StartAsync(
215225
CancellationToken cancellationToken)
216226
{
217-
await base.StartAsync(cancellationToken);
218-
healthService.SetRunningState(nameof(TimeFileWorker), isRunning: true);
227+
return base.StartAsync(cancellationToken);
219228
}
220229

221-
public override async Task StopAsync(
230+
public override Task StopAsync(
222231
CancellationToken cancellationToken)
223232
{
224-
await base.StopAsync(cancellationToken);
225-
healthService.SetRunningState(nameof(TimeFileWorker), isRunning: false);
233+
return base.StopAsync(cancellationToken);
226234
}
227235

228-
public override async Task DoWorkAsync(
236+
public override Task DoWorkAsync(
229237
CancellationToken stoppingToken)
230238
{
231239
var isServiceRunning = healthService.IsServiceRunning(nameof(TimeFileWorker));
@@ -238,9 +246,20 @@ public class TimeFileWorker : BackgroundServiceBase<TimeFileWorker>
238246
workerOptions.OutputDirectory,
239247
$"{time:yyyy-MM-dd--HHmmss}-{isServiceRunning}.txt");
240248

241-
await File.WriteAllTextAsync(outFile, $"{ServiceName}-{isServiceRunning}", stoppingToken);
249+
return File.WriteAllTextAsync(outFile, $"{ServiceName}-{isServiceRunning}", stoppingToken);
250+
}
251+
252+
protected override Task OnExceptionAsync(
253+
Exception exception,
254+
CancellationToken stoppingToken)
255+
{
256+
if (exception is IOException or UnauthorizedAccessException)
257+
{
258+
logger.LogCritical(exception, "Could not write file!");
259+
return StopAsync(stoppingToken);
260+
}
242261

243-
healthService.SetRunningState(nameof(TimeFileWorker), isRunning: true);
262+
return base.OnExceptionAsync(exception, stoppingToken);
244263
}
245264
}
246265
```

sample/.editorconfig

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,5 @@
4949
dotnet_diagnostic.CA1062.severity = none # Validate arguments of public methods
5050
dotnet_diagnostic.CA1852.severity = none # Seal internal types
5151

52-
dotnet_diagnostic.MA0076.severity = none # Do not use implicit culture-sensitive ToString in interpolated strings
52+
dotnet_diagnostic.MA0076.severity = none # Do not use implicit culture-sensitive ToString in interpolated strings
53+
dotnet_diagnostic.CA1848.severity = none # Performance logging

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

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ namespace Atc.Hosting.TimeFile.Sample;
22

33
public class TimeFileWorker : BackgroundServiceBase<TimeFileWorker>
44
{
5-
private readonly IBackgroundServiceHealthService healthService;
65
private readonly ITimeProvider timeProvider;
76

87
private readonly TimeFileWorkerOptions workerOptions;
@@ -14,28 +13,26 @@ public TimeFileWorker(
1413
IOptions<TimeFileWorkerOptions> workerOptions)
1514
: base(
1615
logger,
17-
workerOptions.Value)
16+
workerOptions.Value,
17+
healthService)
1818
{
19-
this.healthService = healthService;
2019
this.timeProvider = timeProvider;
2120
this.workerOptions = workerOptions.Value;
2221
}
2322

24-
public override async Task StartAsync(
23+
public override Task StartAsync(
2524
CancellationToken cancellationToken)
2625
{
27-
await base.StartAsync(cancellationToken);
28-
healthService.SetRunningState(nameof(TimeFileWorker), isRunning: true);
26+
return base.StartAsync(cancellationToken);
2927
}
3028

31-
public override async Task StopAsync(
29+
public override Task StopAsync(
3230
CancellationToken cancellationToken)
3331
{
34-
await base.StopAsync(cancellationToken);
35-
healthService.SetRunningState(nameof(TimeFileWorker), isRunning: false);
32+
return base.StopAsync(cancellationToken);
3633
}
3734

38-
public override async Task DoWorkAsync(
35+
public override Task DoWorkAsync(
3936
CancellationToken stoppingToken)
4037
{
4138
var isServiceRunning = healthService.IsServiceRunning(nameof(TimeFileWorker));
@@ -48,8 +45,19 @@ public override async Task DoWorkAsync(
4845
workerOptions.OutputDirectory,
4946
$"{time:yyyy-MM-dd--HHmmss}-{isServiceRunning}.txt");
5047

51-
await File.WriteAllTextAsync(outFile, $"{ServiceName}-{isServiceRunning}", stoppingToken);
48+
return File.WriteAllTextAsync(outFile, $"{ServiceName}-{isServiceRunning}", stoppingToken);
49+
}
50+
51+
protected override Task OnExceptionAsync(
52+
Exception exception,
53+
CancellationToken stoppingToken)
54+
{
55+
if (exception is IOException or UnauthorizedAccessException)
56+
{
57+
logger.LogCritical(exception, "Could not write file!");
58+
return StopAsync(stoppingToken);
59+
}
5260

53-
healthService.SetRunningState(nameof(TimeFileWorker), isRunning: true);
61+
return base.OnExceptionAsync(exception, stoppingToken);
5462
}
5563
}

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,8 @@ public class TimeFileWorkerOptions : IBackgroundServiceOptions
88

99
public ushort StartupDelaySeconds { get; set; } = 1;
1010

11-
public ushort RetryCount { get; set; } = 3;
12-
1311
public ushort RepeatIntervalSeconds { get; set; } = 20;
1412

1513
public override string ToString()
16-
=> $"{nameof(OutputDirectory)}: {OutputDirectory}, {nameof(StartupDelaySeconds)}: {StartupDelaySeconds}, {nameof(RetryCount)}: {RetryCount}, {nameof(RepeatIntervalSeconds)}: {RepeatIntervalSeconds}";
14+
=> $"{nameof(OutputDirectory)}: {OutputDirectory}, {nameof(StartupDelaySeconds)}: {StartupDelaySeconds}, {nameof(RepeatIntervalSeconds)}: {RepeatIntervalSeconds}";
1715
}

src/Atc.Hosting/Atc.Hosting.csproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
<PackageReference Include="Atc" Version="2.0.349" />
1818
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
1919
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
20-
<PackageReference Include="Polly" Version="7.2.3" />
2120
</ItemGroup>
2221

2322
</Project>

0 commit comments

Comments
 (0)