Simplifies the registration of fire-and-forget background tasks in .NET applications.
For ASP.NET Core, add a reference to the IntelligentPlant.BackgroundTasks.AspNetCore NuGet package and register the services in your application's Startup.cs
file:
services.AddAspNetCoreBackgroundTaskService();
Logging is automatically added for all events related to work items (enqueueing, dequeueing, running, completion, errors).
You can also configure the BackgroundTaskServiceOptions passed to the service to perform additional event handling as follows:
services.AddAspNetCoreBackgroundTaskService(options => {
options.OnRunning = (workItem) => {
// Add custom logic here...
};
options.OnError = (workItem, error) => {
// Add custom logic here...
}
});
Inject the IBackgroundTaskService service into the class that you want to register the task from. Use one of the QueueBackgroundWorkItem
overloads to enqueue the task:
public class EmailNotifier {
private readonly IBackgroundTaskService _backgroundTaskService;
public EmailNotifier(IBackgroundTaskService backgroundTaskService) {
_backgroundTaskService = backgroundTaskService;
}
public void SendEmail(string recipient, string subject, string content) {
_backgroundTaskService.QueueBackgroundWorkItem(async ct => {
// The provided cancellation token will fire when the application is shutting down.
await SendEmailInternal(recipient, subject, content, ct);
});
}
}
The BackgroundWorkItem type represents a work item that will run in a background task. All overloads of QueueBackgroundWorkItem
(other than those that accept a BackgroundWorkItem
instance) return a BackgroundWorkItem
instance that represents the work item that has been queued. This can be used to cancel the work item or wait for it to complete if required. See the sections below for more information.
IMPORTANT:
BackgroundWorkItem
implementsIDisposable
. The background task service will automatically dispose of work items once they have completed. You may also manually dispose of work items if required.
By default, the CancellationToken
provided to the work item will fire when the application is shutting down. You can also explicitly or implicitly cancel work items using the methods described below.
IMPORTANT: If a work item is cancelled before it reaches the front of the background task service's queue, its delegate will not be executed by default. This behaviour can be controlled by an
AppContext
switch (see below).
The BackgroundWorkItem
type defines a Cancel
method that can be used to explicitly cancel the work item. For example:
public class MyClass : IDisposable {
private readonly BackgroundWorkItem _workItem;
public MyClass(IBackgroundTaskService backgroundTaskService) {
_workItem = backgroundTaskService.QueueBackgroundWorkItem(LongRunningTask);
}
private async Task LongRunningTask(CancellationToken cancellationToken) {
while (!cancellationToken.IsCancellationRequested) {
// Do long-running work.
}
}
public void Dispose() {
_workItem.Cancel();
_workItem.Dispose();
}
}
Calling the BackgroundWorkItem.Dispose
method will also cancel the work item if it has not already completed. Note however that calling Dispose
will also immediately mark the work item's Task
property as cancelled if it has not already completed. This means that if you are using the Task
property to wait for the work item to complete (see below), you should not call Dispose
and instead allow the background task service to dispose of the work item when it has completed.
Cancelling a work item does not guarantee that the work item will stop executing immediately. The work item's delegate must react to the cancellation request and stop executing. Cancelling a work item that has already completed has no effect.
The BackgroundTaskServiceExtensions class defines QueueBackgroundWorkItem
extension method overloads that allow you to specify additional CancellationToken
instances for the work item. When registering a work item in this way, a composite of the master token and all of the additional tokens is passed to the work item. Examples of when you might want to use this functionality include starting a long-running background task from an object that should stop when the object is disposed. For example:
public class MyClass : IDisposable {
private bool _disposed;
private readonly CancellationTokenSource _shutdownSource = new CancellationTokenSource();
public MyClass(IBackgroundTaskService backgroundTaskService) {
backgroundTaskService.QueueBackgroundWorkItem(
LongRunningTask,
_shutdownSource.Token
);
}
private async Task LongRunningTask(CancellationToken cancellationToken) {
while (!cancellationToken.IsCancellationRequested) {
// Do long-running work.
}
}
public void Dispose() {
if (_disposed) {
return;
}
_shutdownSource.Cancel();
_shutdownSource.Dispose();
_disposed = true;
}
}
By default, work items will not be invoked if they are cancelled (either explicitly or implicitly via an associated cancellation token) before they reach the front of the background task service's queue. If your application requires that work items are invoked even if cancellation is immediately requested, this can be enabled by setting the IntelligentPlant.BackgroundTasks.BackgroundTaskService.InvokeCancelledWorkItems
switch to true
in the application's AppContext
. For example:
AppContext.SetSwitch("IntelligentPlant.BackgroundTasks.BackgroundTaskService.InvokeCancelledWorkItems", true);
There are a couple of important things to note about this behaviour:
- Background task services will only honour the switch if it is set before the service is created.
- Calling
Dispose
on a service derived from theBackgroundTaskService
base class will always result in queued work items being immediately cancelled and not invoked.
The BackgroundWorkItem type has a Task
property that can be used to wait for the work item to complete. For example:
public class MyClass : IAsyncDisposable {
private readonly CancellationTokenSource _shutdownSource = new CancellationTokenSource();
private readonly Task _workItemTask;
public MyClass(IBackgroundTaskService backgroundTaskService) {
var workItem = backgroundTaskService.QueueBackgroundWorkItem(
LongRunningTask,
_shutdownSource.Token
);
_workItemTask = workItem.Task;
}
private async Task LongRunningTask(CancellationToken cancellationToken) {
try {
while (!cancellationToken.IsCancellationRequested) {
// Do long-running work.
}
}
finally {
// Perform cleanup here.
}
}
public async ValueTask DisposeAsync() {
_shutdownSource.Cancel();
await _workItemTask;
_shutdownSource.Dispose();
}
}
Alternatively, you can use synchronization primitives such as SemaphoreSlim
to wait for work items to complete. For example:
public class MyClass : IAsyncDisposable {
private readonly CancellationTokenSource _shutdownSource = new CancellationTokenSource();
private readonly SemaphoreSlim _workItemSemaphore = new SemaphoreSlim(0);
public MyClass(IBackgroundTaskService backgroundTaskService) {
backgroundTaskService.QueueBackgroundWorkItem(
LongRunningTask,
_shutdownSource.Token
);
}
private async Task LongRunningTask(CancellationToken cancellationToken) {
try {
while (!cancellationToken.IsCancellationRequested) {
// Do long-running work.
}
}
finally {
// Perform cleanup here.
_workItemSemaphore.Release();
}
}
public async ValueTask DisposeAsync() {
_shutdownSource.Cancel();
await _workItemSemaphore.WaitAsync();
_shutdownSource.Dispose();
_workItemSemaphore.Dispose();
}
}
The IntelligentPlant.BackgroundTasks
event source emits events when background work items are enqueued, dequeued, started, and completed. The BackgroundTaskService.EventIds
class contains constants for the different event IDs that can be emitted. See here for more information about event sources.
OpenTelemetry-compatible tracing and metrics for background work items is provided via the System.Diagnostics.DiagnosticSource NuGet package.
To enable OpenTelemetry tracing and metrics instrumentation in an ASP.NET Core application:
- Add a reference to the IntelligentPlant.BackgroundTasks.OpenTelemetry NuGet package.
- If you want to enable tracing for ASP.NET Core requests, follow the instructions for enabling ASP.NET Core OpenTelemetry instrumentation here.
- In your
ConfigureServices
method in yourStartup.cs
file, enable instrumentation for the customActivitySource
instances you want to record, and enable the collection of background task service metrics:
using IntelligentPlant.BackgroundTasks;
// Assumes that you have already created an OpenTelemetry resource builder.
services.AddOpenTelemetry()
.WithTracing(builder => {
builder
.SetResourceBuilder(resourceBuilder)
.AddAspNetCoreInstrumentation()
.AddSource("MyCompany.EmailNotifier", "MyCompany.MyClass")
// Configure OpenTelemetry trace exporters here e.g.
.AddConsoleExporter();
})
.WithMetrics(builder => {
builder
.SetResourceBuilder(resourceBuilder)
.AddBackgroundTaskServiceInstrumentation()
// Configure OpenTelemetry metrics exporters here e.g.
.AddConsoleExporter((exporterOptions, readerOptions) => {
readerOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = 5000;
readerOptions.TemporalityPreference = MetricReaderTemporalityPreference.Cumulative;
});
});
Consider our EmailNotifier
class from above. We can enhance this class so that it can generate Activity
objects for use in traces:
public class EmailNotifier {
private static readonly ActivitySource s_activitySource { get; } = new ActivitySource("MyCompany.EmailNotifier", "1.0.0");
private readonly IBackgroundTaskService _backgroundTaskService;
public EmailNotifier(IBackgroundTaskService backgroundTaskService) {
_backgroundTaskService = backgroundTaskService;
}
public void SendEmail(string recipient, string subject, string content) {
_backgroundTaskService.QueueBackgroundWorkItem(async ct => {
using (s_activitySource.StartActivity("send_email")) {
Activity.Current?.SetTag("recipient", recipient);
// The provided cancellation token will fire when the application is shutting down.
await SendEmailInternal(recipient, subject, content, ct);
}
}, captureParentActivity: true);
}
}
By setting the captureParentActivity
parameter to true
, we are saying that we want to capture the value of Activity.Current
at the moment we call QueueBackgroundWorkItem
, and then restore it as the current activity when the work item is run. This means that the activity that we create inside the work item will be created as a child of the activity that we captured, rather than being created as a new top-level activity.
The following metric instruments are defined:
Total_Queued_Items
- total number of work items that have been added to the background task service's queue since observation began.Total_Dequeued_Items
- total number of work items that the background task service has dequeued since observation began.Total_Started_Items
- total number of work items that the background task service has started executing since observation began.Total_Completed_Items
- total number of work items that have either run to completion or faulted since observation began.Total_Completed_Items_Success
- total number of work items that have run to completion since observation began.Total_Completed_Items_Fail
- total number of work items that have faulted since observation began.Running_Items
- current numer of running work items.Processing_Time
- histogram that records the time that a completed or faulted work item ran for.Queue_Size
- current number of pending work items.
Each sample emitted by any of the above instruments has a tag named IntelligentPlant.BackgroundTasks.Service_Name
that defines the name of the background task service that provided the sample. This can be useful in scenarios where multiple background task service instances exist in an application.