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.
- Atc.Hosting
- Table of Contents
- BackgroundServiceBase
<T>
- BackgroundServiceHealthService
- Complete TimeFileWorker example
- Extensions for ServiceProvider
- Requirements
- How to contribute
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.
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
- To get help with defining a cron expression, use this cron online helper
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
│ │ │ │ │ │
* * * * * *
- Utilizes
ILogger<T>
for type-specific, high-performance logging. - Automatically enriches log entries with the name of the service type (
T
).
- Catches unhandled exceptions and logs them with a severity of
LogLevel.Warning
. - Reruns the
DoWorkAsync
method after a configurablerepeat interval
forBackgroundServiceBase
orscheduled
forBackgroundScheduleServiceBase
. - For manual error handling hook into the exception handling in
DoWorkAsync
by overriding theOnExceptionAsync
method. - Designed to log errors rather than crashing the service.
- Allows for
startup delays
forBackgroundServiceBase
. - Configurable
repeat interval
for running tasks withBackgroundServiceBase
. - Configurable
cron expression
for scheduling running tasks withBackgroundScheduleServiceBase
.
- Simple to derive from and implement your work in the
DoWorkAsync
method.
public class MyBackgroundService : BackgroundServiceBase<MyBackgroundService>
{
public MyBackgroundService(ILogger<MyBackgroundService> logger, IBackgroundServiceOptions options)
: base(logger, options)
{
}
public override Task DoWorkAsync(CancellationToken stoppingToken)
{
// Your background task logic here
}
}
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
}
}
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<TimeFileWorkerOptions>(configuration.GetSection(TimeFileWorkerOptions.SectionName));
services.AddHostedService<TimeFileWorker>();
})
.Build();
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
.
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
.
IBackgroundServiceHealthService
is an interface that provides methods to manage and monitor the health of background services in a .NET application.
SetMaxStalenessInSeconds(string serviceName, ushort seconds)
: Set the maximum allowed staleness duration for a service in seconds.SetRunningState(string serviceName, bool isRunning)
: Update the running state of a service.IsServiceRunning(string serviceName)
: Check if a service is running or not.
Include the following code snippet in your startup to wire up the BackgroundService incl. HealthService.
var host = Host
.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
//...existing dependency injection
services.AddSingleton<IBackgroundServiceHealthService, BackgroundServiceHealthService>(s =>
{
var healthService = new BackgroundServiceHealthService(s.GetRequiredService<ITimeProvider>());
var timeFileWorkerOptions = s.GetRequiredService<IOptions<TimeFileWorkerOptions>>().Value;
healthService.SetMaxStalenessInSeconds(nameof(TimeFileWorker), timeFileWorkerOptions.RepeatIntervalSeconds);
return healthService;
});
})
.Build();
Here, the maximum staleness for TimeFileWorker
is set using its RepeatIntervalSeconds
.
You can utilize IBackgroundServiceHealthService
within your background services as shown below.
public TimeFileWorker(
//...other parameters
IBackgroundServiceHealthService healthService,
//...other parameters
)
{
this.healthService = healthService;
//...other initializations
}
public override async Task StartAsync(CancellationToken cancellationToken)
{
await base.StartAsync(cancellationToken);
healthService.SetRunningState(nameof(TimeFileWorker), isRunning: true);
//...other code
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
//...other code
await base.StopAsync(cancellationToken);
healthService.SetRunningState(nameof(TimeFileWorker), isRunning: false);
}
Inside your worker method, you can set the running state of the service (to update latest timestamp)
public override async Task DoWorkAsync(CancellationToken stoppingToken)
{
//...other code
healthService.SetRunningState(nameof(TimeFileWorker), isRunning: true);
}
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.
public TimeFileWorker(
//...other parameters
IBackgroundServiceHealthService healthService,
//...other parameters
)
: base (healthService)
{
//...other initializations
}
Now you not have to set the running state of the service in the BackgroundService.StartAsync
and BackgroundService.StopAsync
methods.
A sample reference implementation can be found in the sample project Atc.Hosting.TimeFile.Sample
which shows an example of the service TimeFileWorker
that uses BackgroundServiceBase
and the IBackgroundServiceHealthService
.
public class TimeFileWorker : BackgroundServiceBase<TimeFileWorker>
{
private readonly ITimeProvider timeProvider;
private readonly TimeFileWorkerOptions workerOptions;
public TimeFileWorker(
ILogger<TimeFileWorker> logger,
IBackgroundServiceHealthService healthService,
ITimeProvider timeProvider,
IOptions<TimeFileWorkerOptions> 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,
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);
}
}
A sample reference implementation can be found in the sample project Atc.Hosting.TimeFile.Sample
which shows an example of the service TimeFileScheduleWorker
that uses BackgroundScheduleServiceBase
and the IBackgroundServiceHealthService
.
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.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);
}
}
Defined extensions methods for ServiceProvider:
GetHostedService
<T>
Example on how to retrieve the BackgroundService from the HttpContext in a MVC controller or MinimalApi endpoint.
In this example we are working with the TimeFileWorker
BackgroundService.
Note: Remember to wire up BackgroundService in Program.cs
by adding this line services.AddHostedService<TimeFileWorker>();
.
Example setup for a MVC controller:
[HttpGet("my-method")]
public void GetMyMethod()
{
var timeFileWorker = httpContext.RequestServices.GetHostedService<TimeFileWorker>();
if (timeFileWorker is not null)
{
// Here we have access to the TimeFileWorker instance.
}
}
Example setup for a MinimalApi endpoint:
public void DefineEndpoints(WebApplication app)
{
app.MapGet("my-method", async httpContext =>
{
var timeFileWorker = httpContext.RequestServices.GetHostedService<TimeFileWorker>();
if (timeFileWorker is not null)
{
// Here we have access to the TimeFileWorker instance.
}
await Task.CompletedTask;
});
}