IHostedService and BackgroundService in C# allow you to run long-running background tasks within your .NET application, often used for things like scheduled jobs, background processing, or periodic cleanup.
Here’s a BackgroundService doing actual work:
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;
public class MyBackgroundService : BackgroundService
{
private readonly ILogger<MyBackgroundService> _logger;
public MyBackgroundService(ILogger<MyBackgroundService> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("MyBackgroundService is starting.");
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("MyBackgroundService is doing background work at: {time}", DateTimeOffset.Now);
// Simulate some work
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
_logger.LogInformation("MyBackgroundService is stopping.");
}
}
To make this service run, you need to register it with the dependency injection container in your Program.cs (or Startup.cs for older .NET Core versions):
// Program.cs for .NET 6+
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
// Register the background service
builder.Services.AddHostedService<MyBackgroundService>();
var app = builder.Build();
// ... rest of your application setup ...
app.Run();
The core problem BackgroundService solves is managing the lifecycle of long-running operations within the .NET host. Traditionally, you might have put such logic in a separate thread or a console application, but integrating it directly into your web or worker application means it benefits from the same host management: graceful startup, shutdown, and dependency injection. BackgroundService abstracts away much of the boilerplate needed to implement IHostedService directly, particularly the StartAsync and StopAsync methods. It provides a single ExecuteAsync method that runs in a loop, allowing you to place your continuous work there. The CancellationToken passed to ExecuteAsync is crucial; it’s signaled by the host when it’s time for the service to shut down gracefully.
The BackgroundService itself is an implementation of IHostedService. When you register a BackgroundService, the .NET host automatically registers it as an IHostedService. The host calls StartAsync on all registered IHostedService implementations during application startup and StopAsync during shutdown. BackgroundService’s implementation of StartAsync simply kicks off the ExecuteAsync method in a separate task and returns, allowing the application to continue starting. StopAsync waits for the task running ExecuteAsync to complete, which it will do when the CancellationToken is signaled.
The ExecuteAsync method is where you put your actual background work. The while (!stoppingToken.IsCancellationRequested) loop is the standard pattern. Inside the loop, you perform your task. If your task is I/O-bound or takes a significant amount of time, you should await it. Crucially, any Task.Delay or other asynchronous operations within this loop must accept the CancellationToken. This ensures that if the host initiates a shutdown, these operations can be interrupted promptly, preventing the application from hanging during shutdown.
The CancellationToken is the linchpin of graceful shutdown. When the host receives a shutdown signal (e.g., from a SIGTERM on Linux, Ctrl+C on Windows, or an API call to /stop), it iterates through all its registered IHostedServices and calls StopAsync on each. BackgroundService’s StopAsync implementation signals the CancellationToken that was passed to ExecuteAsync. This causes the !stoppingToken.IsCancellationRequested condition to become false, breaking the loop in ExecuteAsync. The ExecuteAsync method then returns, and StopAsync completes, signaling to the host that this service has finished its shutdown process.
When you’re debugging, if your background service isn’t starting, the first thing to check is your DI registration in Program.cs or Startup.cs. Ensure you have builder.Services.AddHostedService<YourBackgroundService>();. If it starts but doesn’t seem to be doing anything, put logging statements inside your ExecuteAsync loop, especially before and after any await operations, and check your application’s output or logs. If it starts and stops immediately, it’s likely an issue with how your ExecuteAsync method is handling the CancellationToken or an exception occurring very early in its execution.
One subtle point is that BackgroundService’s ExecuteAsync is designed to run once and then return. The while loop is what makes it appear continuous. If your background task is a discrete, single operation that needs to run periodically, you might implement a timer within ExecuteAsync or have ExecuteAsync perform one task and then exit, with a separate mechanism (like a scheduled job runner library) triggering ExecuteAsync again. However, for most "run continuously" scenarios, the while loop with Task.Delay is the idiomatic approach.
If your background service is crashing unexpectedly and you can’t see why, ensure you’re catching exceptions within your ExecuteAsync loop and logging them. An unhandled exception within the ExecuteAsync task will cause the host to terminate.
The next thing you’ll likely encounter is managing multiple background services, ensuring they start and stop in a predictable order, or handling dependencies between them.