Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
By Jeow Li Huan
Note
This isn't the latest version of this article. For the current release, see the .NET 10 version of this article.
Warning
This version of ASP.NET Core is no longer supported. For more information, see the .NET and .NET Core Support Policy. For the current release, see the .NET 10 version of this article.
In ASP.NET Core, background tasks can be implemented as hosted services. A hosted service is a class with background task logic that implements the IHostedService interface. Hosted services can be used in Worker Service apps and in web apps. This article provides three hosted service examples:
- Background task that runs on a timer.
- Hosted service that activates a scoped service. The scoped service can use dependency injection (DI).
- Queued background tasks that run sequentially.
Worker Service template
The ASP.NET Core Worker Service template provides a starting point for writing long running service apps. An app created from the Worker Service template specifies the Worker SDK in its project file:
<Project Sdk="Microsoft.NET.Sdk.Worker">
To use the template as a basis for a hosted services app:
- Create a new project.
- Select Worker Service. Select Next.
- Provide a project name in the Project name field or accept the default project name. Select Next.
- In the Additional information dialog, Choose a Framework. Select Create.
Package
An app based on the Worker Service template uses the Microsoft.NET.Sdk.Worker SDK and has an explicit package reference to the Microsoft.Extensions.Hosting NuGet package. For example, see the Worker Service sample app's project file (BackgroundTasksSample.csproj).
For web apps that use the Microsoft.NET.Sdk.Web SDK, the Microsoft.Extensions.Hosting NuGet package is referenced implicitly from the shared framework. An explicit package reference in the app's project file isn't required.
Hosted services in a web app
Hosted services aren't limited to Worker Service apps. A web app that uses the Microsoft.NET.Sdk.Web SDK registers hosted services in the Program file by calling the same AddHostedService extension method on WebApplicationBuilder.Services. The host automatically starts and stops registered hosted services, as described in the IHostedService interface section.
The following Program file is from the web sample app (BackgroundTasksWebSample). It registers the three hosted services that are described in the rest of this article, maps a root endpoint that returns a status message, and adds an endpoint that queues a work item for one of them:
using BackgroundTasksWebSample.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHostedService<TimedHostedService>();
builder.Services.AddHostedService<ConsumeScopedServiceHostedService>();
builder.Services.AddScoped<IScopedProcessingService, ScopedProcessingService>();
builder.Services.AddHostedService<QueuedHostedService>();
builder.Services.AddSingleton<IBackgroundTaskQueue>(_ =>
new BackgroundTaskQueue(builder.Configuration.GetValue("QueueCapacity", 100)));
var app = builder.Build();
app.MapGet("/", () => "Hosted service is running in the background.");
app.MapPost("/queue", async (
IBackgroundTaskQueue taskQueue,
ILogger<Program> logger,
CancellationToken cancellationToken) =>
{
await taskQueue.QueueBackgroundWorkItemAsync(async token =>
{
var guid = Guid.NewGuid();
logger.LogInformation("Queued Background Task {Guid} is starting.", guid);
try
{
await Task.Delay(TimeSpan.FromSeconds(5), token);
}
catch (OperationCanceledException)
{
logger.LogInformation("Queued Background Task {Guid} was cancelled.", guid);
return;
}
logger.LogInformation("Queued Background Task {Guid} is complete.", guid);
}, cancellationToken);
return Results.Accepted();
});
app.Run();
In the preceding code:
TimedHostedService,ConsumeScopedServiceHostedService, andQueuedHostedServiceare registered with the AddHostedService extension method.- A hosted service is registered as a singleton, so it can't receive scoped services, such as an Entity Framework Core
DbContext, by constructor injection.IScopedProcessingServiceis registered as a scoped service, andConsumeScopedServiceHostedServicecreates a scope to resolve it. For more information, see the Consuming a scoped service in a background task section. - The
IBackgroundTaskQueuesingleton is consumed by the/queueendpoint andQueuedHostedService. APOSTrequest to/queuereturns a202 Acceptedresponse once the work item is enqueued without waiting for it to execute. If the bounded queue is full, the request waits until space is available to enqueue the work item.QueuedHostedServiceruns the queued work items in the background. For more information, see the Queued background tasks section.
The following service registration snippets are from the Worker Service sample app (BackgroundTasksSample), where services is the IServiceCollection parameter passed to ConfigureServices. In a web app, use builder.Services instead of services.
IHostedService interface
The IHostedService interface defines two methods for objects that are managed by the host:
StartAsync
StartAsync(CancellationToken) contains the logic to start the background task. StartAsync is called before:
- The app's request processing pipeline is configured.
- The server is started and IApplicationLifetime.ApplicationStarted is triggered.
StartAsync should be limited to short running tasks because hosted services are run sequentially, and no further services are started until StartAsync runs to completion.
Hosted service instances start in the order that they're registered in the dependency injection container unless the app opts into concurrent startup by setting ServicesStartConcurrently to true:
builder.Services.Configure<HostOptions>(options =>
{
options.ServicesStartConcurrently = true;
});
StopAsync
- StopAsync(CancellationToken) is triggered when the host is performing a graceful shutdown.
StopAsynccontains the logic to end the background task. Implement IDisposable and finalizers (destructors) to dispose of any unmanaged resources.
The cancellation token has a default 30 second timeout to indicate that the shutdown process should no longer be graceful. When cancellation is requested on the token:
- Any remaining background operations that the app is performing should be aborted.
- Any methods called in
StopAsyncshould return promptly.
However, tasks aren't abandoned after cancellation is requested—the caller awaits all tasks to complete.
If the app shuts down unexpectedly (for example, the app's process fails), StopAsync might not be called. Therefore, any methods called or operations conducted in StopAsync might not occur.
To extend the default 30 second shutdown timeout, set:
- ShutdownTimeout when using Generic Host. For more information, see .NET Generic Host in ASP.NET Core.
- Shutdown timeout host configuration setting when using Web Host. For more information, see ASP.NET Core Web Host.
The hosted service is activated once at app startup and gracefully shut down at app shutdown. If an error is thrown during background task execution, Dispose should be called even if StopAsync isn't called.
Hosted service instances stop in the reverse order that they're registered in the dependency injection container unless the app opts into concurrent shutdown behavior by setting ServicesStopConcurrently to true:
builder.Services.Configure<HostOptions>(options =>
{
options.ServicesStopConcurrently = true;
});
BackgroundService base class
BackgroundService is a base class for implementing a long running IHostedService.
ExecuteAsync(CancellationToken) is called on the thread pool to run the background service. The implementation returns a Task that represents the entire lifetime of the background service. The host blocks in StopAsync(CancellationToken) waiting for ExecuteAsync to complete.
ExecuteAsync(CancellationToken) is called to run the background service. The implementation returns a Task that represents the entire lifetime of the background service. No further services are started until ExecuteAsync becomes asynchronous, such as by calling await. Avoid performing long, blocking initialization work in ExecuteAsync. The host blocks in StopAsync(CancellationToken) waiting for ExecuteAsync to complete.
The cancellation token is triggered when IHostedService.StopAsync is called. Your implementation of ExecuteAsync should finish promptly when the cancellation token is fired in order to gracefully shut down the service. Otherwise, the service ungracefully shuts down at the shutdown timeout. For more information, see the IHostedService interface section.
For more information, see the BackgroundService source code.
Timed background tasks
A timed background task makes use of the System.Threading.Timer class. The timer triggers the task's DoWork method. The timer is disabled on StopAsync and disposed when the service container is disposed on Dispose:
public class TimedHostedService(ILogger<TimedHostedService> logger) : IHostedService, IDisposable
{
private int executionCount = 0;
private Timer? _timer = null;
public Task StartAsync(CancellationToken stoppingToken)
{
logger.LogInformation("Timed Hosted Service running.");
_timer = new Timer(DoWork, null, TimeSpan.Zero,
TimeSpan.FromSeconds(5));
return Task.CompletedTask;
}
private void DoWork(object? state)
{
var count = Interlocked.Increment(ref executionCount);
logger.LogInformation(
"Timed Hosted Service is working. Count: {Count}", count);
}
public Task StopAsync(CancellationToken stoppingToken)
{
logger.LogInformation("Timed Hosted Service is stopping.");
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
}
The Timer doesn't wait for previous executions of DoWork to finish, so the approach shown might not be suitable for every scenario. Interlocked.Increment is used to increment the execution counter as an atomic operation, which ensures that multiple threads don't update executionCount concurrently.
The service is registered in the Program file with the AddHostedService extension method:
services.AddHostedService<TimedHostedService>();
Consuming a scoped service in a background task
To use scoped services within a BackgroundService, create a scope. No scope is created for a hosted service by default.
The scoped background task service contains the background task's logic. In the following example:
- The service is asynchronous. The
DoWorkmethod returns aTask. For demonstration purposes, a delay of ten seconds is awaited in theDoWorkmethod. - An ILogger is injected into the service.
internal interface IScopedProcessingService
{
Task DoWork(CancellationToken stoppingToken);
}
internal class ScopedProcessingService(
ILogger<ScopedProcessingService> logger) : IScopedProcessingService
{
private int executionCount = 0;
public async Task DoWork(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
executionCount++;
logger.LogInformation(
"Scoped Processing Service is working. Count: {Count}", executionCount);
await Task.Delay(10000, stoppingToken);
}
}
}
The hosted service creates a scope to resolve the scoped background task service to call its DoWork method. DoWork returns a Task, which is awaited in ExecuteAsync:
public class ConsumeScopedServiceHostedService(
IServiceProvider services,
ILogger<ConsumeScopedServiceHostedService> logger) : BackgroundService
{
public IServiceProvider Services { get; } = services;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogInformation(
"Consume Scoped Service Hosted Service running.");
await DoWork(stoppingToken);
}
private async Task DoWork(CancellationToken stoppingToken)
{
logger.LogInformation(
"Consume Scoped Service Hosted Service is working.");
using (var scope = Services.CreateScope())
{
var scopedProcessingService =
scope.ServiceProvider
.GetRequiredService<IScopedProcessingService>();
await scopedProcessingService.DoWork(stoppingToken);
}
}
public override async Task StopAsync(CancellationToken stoppingToken)
{
logger.LogInformation(
"Consume Scoped Service Hosted Service is stopping.");
await base.StopAsync(stoppingToken);
}
}
The services are registered in the Program file. The hosted service is registered with the AddHostedService extension method:
services.AddHostedService<ConsumeScopedServiceHostedService>();
services.AddScoped<IScopedProcessingService, ScopedProcessingService>();
Queued background tasks
A background task queue is based on the .NET Framework 4.x QueueBackgroundWorkItem:
public interface IBackgroundTaskQueue
{
ValueTask QueueBackgroundWorkItemAsync(Func<CancellationToken, ValueTask> workItem);
ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
CancellationToken cancellationToken);
}
public class BackgroundTaskQueue(int capacity) : IBackgroundTaskQueue
{
// Capacity should be set based on the expected application load and
// number of concurrent threads accessing the queue.
// BoundedChannelFullMode.Wait causes calls to WriteAsync() to return a task,
// which only completes when space becomes available. In the event that many
// publishers/calls start accumulating, using BoundedChannelFullMode.Wait can
// lead to backpressure.
private readonly Channel<Func<CancellationToken, ValueTask>> _queue =
Channel.CreateBounded<Func<CancellationToken, ValueTask>>(new BoundedChannelOptions(capacity)
{
FullMode = BoundedChannelFullMode.Wait
});
public async ValueTask QueueBackgroundWorkItemAsync(
Func<CancellationToken, ValueTask> workItem)
{
if (workItem == null)
{
throw new ArgumentNullException(nameof(workItem));
}
await _queue.Writer.WriteAsync(workItem);
}
public async ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
CancellationToken cancellationToken)
{
var workItem = await _queue.Reader.ReadAsync(cancellationToken);
return workItem;
}
}
In the following QueueHostedService example:
- The
BackgroundProcessingmethod returns aTask, which is awaited inExecuteAsync. - Background tasks in the queue are dequeued and executed in
BackgroundProcessing. - Work items are awaited before the service stops in
StopAsync.
public class QueuedHostedService(
IBackgroundTaskQueue taskQueue,
ILogger<QueuedHostedService> logger) : BackgroundService
{
public IBackgroundTaskQueue TaskQueue { get; } = taskQueue;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogInformation(
$"Queued Hosted Service is running.{Environment.NewLine}" +
$"{Environment.NewLine}Tap W to add a work item to the " +
$"background queue.{Environment.NewLine}");
await BackgroundProcessing(stoppingToken);
}
private async Task BackgroundProcessing(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var workItem =
await TaskQueue.DequeueAsync(stoppingToken);
try
{
await workItem(stoppingToken);
}
catch (Exception ex)
{
logger.LogError(ex,
"Error occurred executing {WorkItem}.", nameof(workItem));
}
}
}
public override async Task StopAsync(CancellationToken stoppingToken)
{
logger.LogInformation("Queued Hosted Service is stopping.");
await base.StopAsync(stoppingToken);
}
}
A MonitorLoop service handles enqueuing tasks for the hosted service whenever the w key is selected on an input device:
- The
IBackgroundTaskQueueis injected into theMonitorLoopservice. IBackgroundTaskQueue.QueueBackgroundWorkItemis called to enqueue a work item.- The work item simulates a long-running background task:
- Three 5-second delays are executed (
Task.Delay). - A
try-catchstatement traps OperationCanceledException if the task is cancelled.
- Three 5-second delays are executed (
public class MonitorLoop(
IBackgroundTaskQueue taskQueue,
ILogger<MonitorLoop> logger,
IHostApplicationLifetime applicationLifetime)
{
private readonly CancellationToken _cancellationToken = applicationLifetime.ApplicationStopping;
public void StartMonitorLoop()
{
logger.LogInformation("MonitorAsync Loop is starting.");
// Run a console user input loop in a background thread
Task.Run(async () => await MonitorAsync());
}
private async ValueTask MonitorAsync()
{
while (!_cancellationToken.IsCancellationRequested)
{
var keyStroke = Console.ReadKey();
if (keyStroke.Key == ConsoleKey.W)
{
// Enqueue a background work item
await taskQueue.QueueBackgroundWorkItemAsync(BuildWorkItem);
}
}
}
private async ValueTask BuildWorkItem(CancellationToken token)
{
// Simulate three 5-second tasks to complete
// for each enqueued work item
int delayLoop = 0;
var guid = Guid.NewGuid().ToString();
logger.LogInformation("Queued Background Task {Guid} is starting.", guid);
while (!token.IsCancellationRequested && delayLoop < 3)
{
try
{
await Task.Delay(TimeSpan.FromSeconds(5), token);
}
catch (OperationCanceledException)
{
// Prevent throwing if the Delay is cancelled
}
delayLoop++;
logger.LogInformation("Queued Background Task {Guid} is running. "
+ "{DelayLoop}/3", guid, delayLoop);
}
if (delayLoop == 3)
{
logger.LogInformation("Queued Background Task {Guid} is complete.", guid);
}
else
{
logger.LogInformation("Queued Background Task {Guid} was cancelled.", guid);
}
}
}
The services are registered in the Program file. The hosted service is registered with the AddHostedService extension method:
services.AddSingleton<MonitorLoop>();
services.AddHostedService<QueuedHostedService>();
services.AddSingleton<IBackgroundTaskQueue>(ctx =>
{
if (!int.TryParse(hostContext.Configuration["QueueCapacity"], out var queueCapacity))
queueCapacity = 100;
return new BackgroundTaskQueue(queueCapacity);
});
MonitorLoop is started in the Program file:
var monitorLoop = host.Services.GetRequiredService<MonitorLoop>();
monitorLoop.StartMonitorLoop();
In a web app, an endpoint can queue work items instead of a console input loop. For an example, see the Hosted services in a web app section.
Asynchronous timed background task
The following code creates an asynchronous timed background task:
namespace TimedBackgroundTasks;
public class TimedHostedService : BackgroundService
{
private readonly ILogger<TimedHostedService> _logger;
private int _executionCount;
public TimedHostedService(ILogger<TimedHostedService> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Timed Hosted Service running.");
// When the timer should have no due-time, then do the work once now.
await DoWork();
using PeriodicTimer timer = new(TimeSpan.FromSeconds(30));
try
{
while (await timer.WaitForNextTickAsync(stoppingToken))
{
await DoWork();
}
}
catch (OperationCanceledException)
{
_logger.LogInformation("Timed Hosted Service is stopping.");
}
}
private async Task DoWork()
{
int count = Interlocked.Increment(ref _executionCount);
// Simulate work
await Task.Delay(TimeSpan.FromSeconds(2));
_logger.LogInformation("Timed Hosted Service is working. Count: {Count}", count);
}
}
Native AOT
The Worker Service templates support .NET native ahead-of-time (AOT) with the --aot flag:
- Create a new project.
- Select Worker Service. Select Next.
- Provide a project name in the Project name field or accept the default project name. Select Next.
- In the Additional information dialog:
- Choose a Framework.
- Check the Enable Native AOT publish checkbox.
- Select Create.
The AOT option adds <PublishAot>true</PublishAot> to the project file:
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<InvariantGlobalization>true</InvariantGlobalization>
+ <PublishAot>true</PublishAot>
<UserSecretsId>dotnet-WorkerWithAot-e94b2</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0-preview.4.23259.5" />
</ItemGroup>
</Project>
Additional resources
In ASP.NET Core, background tasks can be implemented as hosted services. A hosted service is a class with background task logic that implements the IHostedService interface. This article provides three hosted service examples:
- Background task that runs on a timer.
- Hosted service that activates a scoped service. The scoped service can use dependency injection (DI).
- Queued background tasks that run sequentially.
Worker Service template
The ASP.NET Core Worker Service template provides a starting point for writing long running service apps. An app created from the Worker Service template specifies the Worker SDK in its project file:
<Project Sdk="Microsoft.NET.Sdk.Worker">
To use the template as a basis for a hosted services app:
- Create a new project.
- Select Worker Service. Select Next.
- Provide a project name in the Project name field or accept the default project name. Select Next.
- In the Additional information dialog, Choose a Framework. Select Create.
Package
An app based on the Worker Service template uses the Microsoft.NET.Sdk.Worker SDK and has an explicit package reference to the Microsoft.Extensions.Hosting package. For example, see the sample app's project file (BackgroundTasksSample.csproj).
For web apps that use the Microsoft.NET.Sdk.Web SDK, the Microsoft.Extensions.Hosting package is referenced implicitly from the shared framework. An explicit package reference in the app's project file isn't required.
IHostedService interface
The IHostedService interface defines two methods for objects that are managed by the host:
StartAsync
StartAsync(CancellationToken) contains the logic to start the background task. StartAsync is called before:
- The app's request processing pipeline is configured.
- The server is started and IApplicationLifetime.ApplicationStarted is triggered.
StartAsync should be limited to short running tasks because hosted services are run sequentially, and no further services are started until StartAsync runs to completion.
StopAsync
- StopAsync(CancellationToken) is triggered when the host is performing a graceful shutdown.
StopAsynccontains the logic to end the background task. Implement IDisposable and finalizers (destructors) to dispose of any unmanaged resources.
The cancellation token has a default 30 second timeout to indicate that the shutdown process should no longer be graceful. When cancellation is requested on the token:
- Any remaining background operations that the app is performing should be aborted.
- Any methods called in
StopAsyncshould return promptly.
However, tasks aren't abandoned after cancellation is requested—the caller awaits all tasks to complete.
If the app shuts down unexpectedly (for example, the app's process fails), StopAsync might not be called. Therefore, any methods called or operations conducted in StopAsync might not occur.
To extend the default 30 second shutdown timeout, set:
- ShutdownTimeout when using Generic Host. For more information, see .NET Generic Host in ASP.NET Core.
- Shutdown timeout host configuration setting when using Web Host. For more information, see ASP.NET Core Web Host.
The hosted service is activated once at app startup and gracefully shut down at app shutdown. If an error is thrown during background task execution, Dispose should be called even if StopAsync isn't called.
BackgroundService base class
BackgroundService is a base class for implementing a long running IHostedService.
ExecuteAsync(CancellationToken) is called to run the background service. The implementation returns a Task that represents the entire lifetime of the background service. No further services are started until ExecuteAsync becomes asynchronous, such as by calling await. Avoid performing long, blocking initialization work in ExecuteAsync. The host blocks in StopAsync(CancellationToken) waiting for ExecuteAsync to complete.
The cancellation token is triggered when IHostedService.StopAsync is called. Your implementation of ExecuteAsync should finish promptly when the cancellation token is fired in order to gracefully shut down the service. Otherwise, the service ungracefully shuts down at the shutdown timeout. For more information, see the IHostedService interface section.
For more information, see the BackgroundService source code.
Timed background tasks
A timed background task makes use of the System.Threading.Timer class. The timer triggers the task's DoWork method. The timer is disabled on StopAsync and disposed when the service container is disposed on Dispose:
public class TimedHostedService : IHostedService, IDisposable
{
private int executionCount = 0;
private readonly ILogger<TimedHostedService> _logger;
private Timer? _timer = null;
public TimedHostedService(ILogger<TimedHostedService> logger)
{
_logger = logger;
}
public Task StartAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Timed Hosted Service running.");
_timer = new Timer(DoWork, null, TimeSpan.Zero,
TimeSpan.FromSeconds(5));
return Task.CompletedTask;
}
private void DoWork(object? state)
{
var count = Interlocked.Increment(ref executionCount);
_logger.LogInformation(
"Timed Hosted Service is working. Count: {Count}", count);
}
public Task StopAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Timed Hosted Service is stopping.");
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
}
The Timer doesn't wait for previous executions of DoWork to finish, so the approach shown might not be suitable for every scenario. Interlocked.Increment is used to increment the execution counter as an atomic operation, which ensures that multiple threads don't update executionCount concurrently.
The service is registered in IHostBuilder.ConfigureServices (Program.cs) with the AddHostedService extension method:
services.AddHostedService<TimedHostedService>();
Consuming a scoped service in a background task
To use scoped services within a BackgroundService, create a scope. No scope is created for a hosted service by default.
The scoped background task service contains the background task's logic. In the following example:
- The service is asynchronous. The
DoWorkmethod returns aTask. For demonstration purposes, a delay of ten seconds is awaited in theDoWorkmethod. - An ILogger is injected into the service.
internal interface IScopedProcessingService
{
Task DoWork(CancellationToken stoppingToken);
}
internal class ScopedProcessingService : IScopedProcessingService
{
private int executionCount = 0;
private readonly ILogger _logger;
public ScopedProcessingService(ILogger<ScopedProcessingService> logger)
{
_logger = logger;
}
public async Task DoWork(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
executionCount++;
_logger.LogInformation(
"Scoped Processing Service is working. Count: {Count}", executionCount);
await Task.Delay(10000, stoppingToken);
}
}
}
The hosted service creates a scope to resolve the scoped background task service to call its DoWork method. DoWork returns a Task, which is awaited in ExecuteAsync:
public class ConsumeScopedServiceHostedService : BackgroundService
{
private readonly ILogger<ConsumeScopedServiceHostedService> _logger;
public ConsumeScopedServiceHostedService(IServiceProvider services,
ILogger<ConsumeScopedServiceHostedService> logger)
{
Services = services;
_logger = logger;
}
public IServiceProvider Services { get; }
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation(
"Consume Scoped Service Hosted Service running.");
await DoWork(stoppingToken);
}
private async Task DoWork(CancellationToken stoppingToken)
{
_logger.LogInformation(
"Consume Scoped Service Hosted Service is working.");
using (var scope = Services.CreateScope())
{
var scopedProcessingService =
scope.ServiceProvider
.GetRequiredService<IScopedProcessingService>();
await scopedProcessingService.DoWork(stoppingToken);
}
}
public override async Task StopAsync(CancellationToken stoppingToken)
{
_logger.LogInformation(
"Consume Scoped Service Hosted Service is stopping.");
await base.StopAsync(stoppingToken);
}
}
The services are registered in IHostBuilder.ConfigureServices (Program.cs). The hosted service is registered with the AddHostedService extension method:
services.AddHostedService<ConsumeScopedServiceHostedService>();
services.AddScoped<IScopedProcessingService, ScopedProcessingService>();
Queued background tasks
A background task queue is based on the .NET Framework 4.x QueueBackgroundWorkItem:
public interface IBackgroundTaskQueue
{
ValueTask QueueBackgroundWorkItemAsync(Func<CancellationToken, ValueTask> workItem);
ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
CancellationToken cancellationToken);
}
public class BackgroundTaskQueue : IBackgroundTaskQueue
{
private readonly Channel<Func<CancellationToken, ValueTask>> _queue;
public BackgroundTaskQueue(int capacity)
{
// Capacity should be set based on the expected application load and
// number of concurrent threads accessing the queue.
// BoundedChannelFullMode.Wait causes calls to WriteAsync() to return a task,
// which only completes when space becomes available. In the event that many
// publishers/calls start accumulating, using BoundedChannelFullMode.Wait can
// lead to backpressure.
var options = new BoundedChannelOptions(capacity)
{
FullMode = BoundedChannelFullMode.Wait
};
_queue = Channel.CreateBounded<Func<CancellationToken, ValueTask>>(options);
}
public async ValueTask QueueBackgroundWorkItemAsync(
Func<CancellationToken, ValueTask> workItem)
{
if (workItem == null)
{
throw new ArgumentNullException(nameof(workItem));
}
await _queue.Writer.WriteAsync(workItem);
}
public async ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
CancellationToken cancellationToken)
{
var workItem = await _queue.Reader.ReadAsync(cancellationToken);
return workItem;
}
}
In the following QueueHostedService example:
- The
BackgroundProcessingmethod returns aTask, which is awaited inExecuteAsync. - Background tasks in the queue are dequeued and executed in
BackgroundProcessing. - Work items are awaited before the service stops in
StopAsync.
public class QueuedHostedService : BackgroundService
{
private readonly ILogger<QueuedHostedService> _logger;
public QueuedHostedService(IBackgroundTaskQueue taskQueue,
ILogger<QueuedHostedService> logger)
{
TaskQueue = taskQueue;
_logger = logger;
}
public IBackgroundTaskQueue TaskQueue { get; }
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation(
$"Queued Hosted Service is running.{Environment.NewLine}" +
$"{Environment.NewLine}Tap W to add a work item to the " +
$"background queue.{Environment.NewLine}");
await BackgroundProcessing(stoppingToken);
}
private async Task BackgroundProcessing(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var workItem =
await TaskQueue.DequeueAsync(stoppingToken);
try
{
await workItem(stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Error occurred executing {WorkItem}.", nameof(workItem));
}
}
}
public override async Task StopAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Queued Hosted Service is stopping.");
await base.StopAsync(stoppingToken);
}
}
A MonitorLoop service handles enqueuing tasks for the hosted service whenever the w key is selected on an input device:
- The
IBackgroundTaskQueueis injected into theMonitorLoopservice. IBackgroundTaskQueue.QueueBackgroundWorkItemis called to enqueue a work item.- The work item simulates a long-running background task:
- Three 5-second delays are executed (
Task.Delay). - A
try-catchstatement traps OperationCanceledException if the task is cancelled.
- Three 5-second delays are executed (
public class MonitorLoop
{
private readonly IBackgroundTaskQueue _taskQueue;
private readonly ILogger _logger;
private readonly CancellationToken _cancellationToken;
public MonitorLoop(IBackgroundTaskQueue taskQueue,
ILogger<MonitorLoop> logger,
IHostApplicationLifetime applicationLifetime)
{
_taskQueue = taskQueue;
_logger = logger;
_cancellationToken = applicationLifetime.ApplicationStopping;
}
public void StartMonitorLoop()
{
_logger.LogInformation("MonitorAsync Loop is starting.");
// Run a console user input loop in a background thread
Task.Run(async () => await MonitorAsync());
}
private async ValueTask MonitorAsync()
{
while (!_cancellationToken.IsCancellationRequested)
{
var keyStroke = Console.ReadKey();
if (keyStroke.Key == ConsoleKey.W)
{
// Enqueue a background work item
await _taskQueue.QueueBackgroundWorkItemAsync(BuildWorkItem);
}
}
}
private async ValueTask BuildWorkItem(CancellationToken token)
{
// Simulate three 5-second tasks to complete
// for each enqueued work item
int delayLoop = 0;
var guid = Guid.NewGuid().ToString();
_logger.LogInformation("Queued Background Task {Guid} is starting.", guid);
while (!token.IsCancellationRequested && delayLoop < 3)
{
try
{
await Task.Delay(TimeSpan.FromSeconds(5), token);
}
catch (OperationCanceledException)
{
// Prevent throwing if the Delay is cancelled
}
delayLoop++;
_logger.LogInformation("Queued Background Task {Guid} is running. "
+ "{DelayLoop}/3", guid, delayLoop);
}
if (delayLoop == 3)
{
_logger.LogInformation("Queued Background Task {Guid} is complete.", guid);
}
else
{
_logger.LogInformation("Queued Background Task {Guid} was cancelled.", guid);
}
}
}
The services are registered in IHostBuilder.ConfigureServices (Program.cs). The hosted service is registered with the AddHostedService extension method:
services.AddSingleton<MonitorLoop>();
services.AddHostedService<QueuedHostedService>();
services.AddSingleton<IBackgroundTaskQueue>(ctx =>
{
if (!int.TryParse(hostContext.Configuration["QueueCapacity"], out var queueCapacity))
queueCapacity = 100;
return new BackgroundTaskQueue(queueCapacity);
});
MonitorLoop is started in Program.cs:
var monitorLoop = host.Services.GetRequiredService<MonitorLoop>();
monitorLoop.StartMonitorLoop();
Asynchronous timed background task
The following code creates an asynchronous timed background task:
namespace TimedBackgroundTasks;
public class TimedHostedService : BackgroundService
{
private readonly ILogger<TimedHostedService> _logger;
private int _executionCount;
public TimedHostedService(ILogger<TimedHostedService> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Timed Hosted Service running.");
// When the timer should have no due-time, then do the work once now.
await DoWork();
using PeriodicTimer timer = new(TimeSpan.FromSeconds(30));
try
{
while (await timer.WaitForNextTickAsync(stoppingToken))
{
await DoWork();
}
}
catch (OperationCanceledException)
{
_logger.LogInformation("Timed Hosted Service is stopping.");
}
}
private async Task DoWork()
{
int count = Interlocked.Increment(ref _executionCount);
// Simulate work
await Task.Delay(TimeSpan.FromSeconds(2));
_logger.LogInformation("Timed Hosted Service is working. Count: {Count}", count);
}
}
Additional resources
In ASP.NET Core, background tasks can be implemented as hosted services. A hosted service is a class with background task logic that implements the IHostedService interface. This article provides three hosted service examples:
- Background task that runs on a timer.
- Hosted service that activates a scoped service. The scoped service can use dependency injection (DI).
- Queued background tasks that run sequentially.
View or download sample code (how to download)
Worker Service template
The ASP.NET Core Worker Service template provides a starting point for writing long running service apps. An app created from the Worker Service template specifies the Worker SDK in its project file:
<Project Sdk="Microsoft.NET.Sdk.Worker">
To use the template as a basis for a hosted services app:
- Create a new project.
- Select Worker Service. Select Next.
- Provide a project name in the Project name field or accept the default project name. Select Create.
- In the Create a new Worker service dialog, select Create.
Package
An app based on the Worker Service template uses the Microsoft.NET.Sdk.Worker SDK and has an explicit package reference to the Microsoft.Extensions.Hosting package. For example, see the sample app's project file (BackgroundTasksSample.csproj).
For web apps that use the Microsoft.NET.Sdk.Web SDK, the Microsoft.Extensions.Hosting package is referenced implicitly from the shared framework. An explicit package reference in the app's project file isn't required.
IHostedService interface
The IHostedService interface defines two methods for objects that are managed by the host:
StartAsync
StartAsync contains the logic to start the background task. StartAsync is called before:
- The app's request processing pipeline is configured.
- The server is started and IApplicationLifetime.ApplicationStarted is triggered.
The default behavior can be changed so that the hosted service's StartAsync runs after the app's pipeline has been configured and ApplicationStarted is called. To change the default behavior, add the hosted service (VideosWatcher in the following example) after calling ConfigureWebHostDefaults:
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
})
.ConfigureServices(services =>
{
services.AddHostedService<VideosWatcher>();
});
}
StopAsync
- StopAsync(CancellationToken) is triggered when the host is performing a graceful shutdown.
StopAsynccontains the logic to end the background task. Implement IDisposable and finalizers (destructors) to dispose of any unmanaged resources.
The cancellation token has a default five second timeout to indicate that the shutdown process should no longer be graceful. When cancellation is requested on the token:
- Any remaining background operations that the app is performing should be aborted.
- Any methods called in
StopAsyncshould return promptly.
However, tasks aren't abandoned after cancellation is requested—the caller awaits all tasks to complete.
If the app shuts down unexpectedly (for example, the app's process fails), StopAsync might not be called. Therefore, any methods called or operations conducted in StopAsync might not occur.
To extend the default five second shutdown timeout, set:
- ShutdownTimeout when using Generic Host. For more information, see .NET Generic Host in ASP.NET Core.
- Shutdown timeout host configuration setting when using Web Host. For more information, see ASP.NET Core Web Host.
The hosted service is activated once at app startup and gracefully shut down at app shutdown. If an error is thrown during background task execution, Dispose should be called even if StopAsync isn't called.
BackgroundService base class
BackgroundService is a base class for implementing a long running IHostedService.
ExecuteAsync(CancellationToken) is called to run the background service. The implementation returns a Task that represents the entire lifetime of the background service. No further services are started until ExecuteAsync becomes asynchronous, such as by calling await. Avoid performing long, blocking initialization work in ExecuteAsync. The host blocks in StopAsync(CancellationToken) waiting for ExecuteAsync to complete.
The cancellation token is triggered when IHostedService.StopAsync is called. Your implementation of ExecuteAsync should finish promptly when the cancellation token is fired in order to gracefully shut down the service. Otherwise, the service ungracefully shuts down at the shutdown timeout. For more information, see the IHostedService interface section.
StartAsync should be limited to short running tasks because hosted services are run sequentially, and no further services are started until StartAsync runs to completion. Long running tasks should be placed in ExecuteAsync. For more information, see the source to BackgroundService.
Timed background tasks
A timed background task makes use of the System.Threading.Timer class. The timer triggers the task's DoWork method. The timer is disabled on StopAsync and disposed when the service container is disposed on Dispose:
public class TimedHostedService : IHostedService, IDisposable
{
private int executionCount = 0;
private readonly ILogger<TimedHostedService> _logger;
private Timer _timer;
public TimedHostedService(ILogger<TimedHostedService> logger)
{
_logger = logger;
}
public Task StartAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Timed Hosted Service running.");
_timer = new Timer(DoWork, null, TimeSpan.Zero,
TimeSpan.FromSeconds(5));
return Task.CompletedTask;
}
private void DoWork(object state)
{
var count = Interlocked.Increment(ref executionCount);
_logger.LogInformation(
"Timed Hosted Service is working. Count: {Count}", count);
}
public Task StopAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Timed Hosted Service is stopping.");
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
}
The Timer doesn't wait for previous executions of DoWork to finish, so the approach shown might not be suitable for every scenario. Interlocked.Increment is used to increment the execution counter as an atomic operation, which ensures that multiple threads don't update executionCount concurrently.
The service is registered in IHostBuilder.ConfigureServices (Program.cs) with the AddHostedService extension method:
services.AddHostedService<TimedHostedService>();
Consuming a scoped service in a background task
To use scoped services within a BackgroundService, create a scope. No scope is created for a hosted service by default.
The scoped background task service contains the background task's logic. In the following example:
- The service is asynchronous. The
DoWorkmethod returns aTask. For demonstration purposes, a delay of ten seconds is awaited in theDoWorkmethod. - An ILogger is injected into the service.
internal interface IScopedProcessingService
{
Task DoWork(CancellationToken stoppingToken);
}
internal class ScopedProcessingService : IScopedProcessingService
{
private int executionCount = 0;
private readonly ILogger _logger;
public ScopedProcessingService(ILogger<ScopedProcessingService> logger)
{
_logger = logger;
}
public async Task DoWork(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
executionCount++;
_logger.LogInformation(
"Scoped Processing Service is working. Count: {Count}", executionCount);
await Task.Delay(10000, stoppingToken);
}
}
}
The hosted service creates a scope to resolve the scoped background task service to call its DoWork method. DoWork returns a Task, which is awaited in ExecuteAsync:
public class ConsumeScopedServiceHostedService : BackgroundService
{
private readonly ILogger<ConsumeScopedServiceHostedService> _logger;
public ConsumeScopedServiceHostedService(IServiceProvider services,
ILogger<ConsumeScopedServiceHostedService> logger)
{
Services = services;
_logger = logger;
}
public IServiceProvider Services { get; }
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation(
"Consume Scoped Service Hosted Service running.");
await DoWork(stoppingToken);
}
private async Task DoWork(CancellationToken stoppingToken)
{
_logger.LogInformation(
"Consume Scoped Service Hosted Service is working.");
using (var scope = Services.CreateScope())
{
var scopedProcessingService =
scope.ServiceProvider
.GetRequiredService<IScopedProcessingService>();
await scopedProcessingService.DoWork(stoppingToken);
}
}
public override async Task StopAsync(CancellationToken stoppingToken)
{
_logger.LogInformation(
"Consume Scoped Service Hosted Service is stopping.");
await base.StopAsync(stoppingToken);
}
}
The services are registered in IHostBuilder.ConfigureServices (Program.cs). The hosted service is registered with the AddHostedService extension method:
services.AddHostedService<ConsumeScopedServiceHostedService>();
services.AddScoped<IScopedProcessingService, ScopedProcessingService>();
Queued background tasks
A background task queue is based on the .NET Framework 4.x QueueBackgroundWorkItem:
public interface IBackgroundTaskQueue
{
ValueTask QueueBackgroundWorkItemAsync(Func<CancellationToken, ValueTask> workItem);
ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
CancellationToken cancellationToken);
}
public class BackgroundTaskQueue : IBackgroundTaskQueue
{
private readonly Channel<Func<CancellationToken, ValueTask>> _queue;
public BackgroundTaskQueue(int capacity)
{
// Capacity should be set based on the expected application load and
// number of concurrent threads accessing the queue.
// BoundedChannelFullMode.Wait causes calls to WriteAsync() to return a task,
// which only completes when space becomes available. In the event that many
// publishers/calls start accumulating, using BoundedChannelFullMode.Wait can
// lead to backpressure.
var options = new BoundedChannelOptions(capacity)
{
FullMode = BoundedChannelFullMode.Wait
};
_queue = Channel.CreateBounded<Func<CancellationToken, ValueTask>>(options);
}
public async ValueTask QueueBackgroundWorkItemAsync(
Func<CancellationToken, ValueTask> workItem)
{
if (workItem == null)
{
throw new ArgumentNullException(nameof(workItem));
}
await _queue.Writer.WriteAsync(workItem);
}
public async ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
CancellationToken cancellationToken)
{
var workItem = await _queue.Reader.ReadAsync(cancellationToken);
return workItem;
}
}
In the following QueueHostedService example:
- The
BackgroundProcessingmethod returns aTask, which is awaited inExecuteAsync. - Background tasks in the queue are dequeued and executed in
BackgroundProcessing. - Work items are awaited before the service stops in
StopAsync.
public class QueuedHostedService : BackgroundService
{
private readonly ILogger<QueuedHostedService> _logger;
public QueuedHostedService(IBackgroundTaskQueue taskQueue,
ILogger<QueuedHostedService> logger)
{
TaskQueue = taskQueue;
_logger = logger;
}
public IBackgroundTaskQueue TaskQueue { get; }
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation(
$"Queued Hosted Service is running.{Environment.NewLine}" +
$"{Environment.NewLine}Tap W to add a work item to the " +
$"background queue.{Environment.NewLine}");
await BackgroundProcessing(stoppingToken);
}
private async Task BackgroundProcessing(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var workItem =
await TaskQueue.DequeueAsync(stoppingToken);
try
{
await workItem(stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex,
"Error occurred executing {WorkItem}.", nameof(workItem));
}
}
}
public override async Task StopAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Queued Hosted Service is stopping.");
await base.StopAsync(stoppingToken);
}
}
A MonitorLoop service handles enqueuing tasks for the hosted service whenever the w key is selected on an input device:
- The
IBackgroundTaskQueueis injected into theMonitorLoopservice. IBackgroundTaskQueue.QueueBackgroundWorkItemis called to enqueue a work item.- The work item simulates a long-running background task:
- Three 5-second delays are executed (
Task.Delay). - A
try-catchstatement traps OperationCanceledException if the task is cancelled.
- Three 5-second delays are executed (
public class MonitorLoop
{
private readonly IBackgroundTaskQueue _taskQueue;
private readonly ILogger _logger;
private readonly CancellationToken _cancellationToken;
public MonitorLoop(IBackgroundTaskQueue taskQueue,
ILogger<MonitorLoop> logger,
IHostApplicationLifetime applicationLifetime)
{
_taskQueue = taskQueue;
_logger = logger;
_cancellationToken = applicationLifetime.ApplicationStopping;
}
public void StartMonitorLoop()
{
_logger.LogInformation("MonitorAsync Loop is starting.");
// Run a console user input loop in a background thread
Task.Run(async () => await MonitorAsync());
}
private async ValueTask MonitorAsync()
{
while (!_cancellationToken.IsCancellationRequested)
{
var keyStroke = Console.ReadKey();
if (keyStroke.Key == ConsoleKey.W)
{
// Enqueue a background work item
await _taskQueue.QueueBackgroundWorkItemAsync(BuildWorkItem);
}
}
}
private async ValueTask BuildWorkItem(CancellationToken token)
{
// Simulate three 5-second tasks to complete
// for each enqueued work item
int delayLoop = 0;
var guid = Guid.NewGuid().ToString();
_logger.LogInformation("Queued Background Task {Guid} is starting.", guid);
while (!token.IsCancellationRequested && delayLoop < 3)
{
try
{
await Task.Delay(TimeSpan.FromSeconds(5), token);
}
catch (OperationCanceledException)
{
// Prevent throwing if the Delay is cancelled
}
delayLoop++;
_logger.LogInformation("Queued Background Task {Guid} is running. " + "{DelayLoop}/3", guid, delayLoop);
}
if (delayLoop == 3)
{
_logger.LogInformation("Queued Background Task {Guid} is complete.", guid);
}
else
{
_logger.LogInformation("Queued Background Task {Guid} was cancelled.", guid);
}
}
}
The services are registered in IHostBuilder.ConfigureServices (Program.cs). The hosted service is registered with the AddHostedService extension method:
services.AddSingleton<MonitorLoop>();
services.AddHostedService<QueuedHostedService>();
services.AddSingleton<IBackgroundTaskQueue>(ctx => {
if (!int.TryParse(hostContext.Configuration["QueueCapacity"], out var queueCapacity))
queueCapacity = 100;
return new BackgroundTaskQueue(queueCapacity);
});
MonitorLoop is started in Program.Main:
var monitorLoop = host.Services.GetRequiredService<MonitorLoop>();
monitorLoop.StartMonitorLoop();
Additional resources
ASP.NET Core