Background Services, Hosted Services & Worker Queues Masterclass
Welcome to Phase 16 (Chapter 44): C# Background Services, IHostedService, BackgroundService & Worker Queues Masterclass! Many production applications need tasks that run continuously in the background โ sending emails, processing queues, syncing data, running scheduled jobs, or monitoring health. In this chapter, we implement IHostedService, BackgroundService, Worker Services, background email processing with Channel<T> queues, scheduled tasks, and graceful shutdown.
| Interface | Methods Required | Best Used For |
|---|---|---|
IHostedService | StartAsync() + StopAsync() | Fine-grained control over start/stop lifecycle |
BackgroundService | ExecuteAsync(CancellationToken) | Long-running loop services (recommended for most cases) |
| Worker Service Template | ExecuteAsync() | Standalone console background worker process |
public class DataSyncService : BackgroundService
{
private readonly ILogger<DataSyncService> _logger;
private readonly TimeSpan _interval = TimeSpan.FromMinutes(5);
public DataSyncService(ILogger<DataSyncService> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("DataSyncService started at {Time}", DateTimeOffset.Now);
while (!stoppingToken.IsCancellationRequested)
{
try
{
_logger.LogInformation("Syncing data at: {Time}", DateTimeOffset.Now);
await SyncDataAsync(stoppingToken);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
_logger.LogError(ex, "Error syncing data");
}
await Task.Delay(_interval, stoppingToken); // Wait 5 minutes
}
_logger.LogInformation("DataSyncService stopped gracefully.");
}
private async Task SyncDataAsync(CancellationToken ct)
{
// Perform database sync, file processing, API polling etc.
await Task.Delay(100, ct); // Simulate work
}
}
// Register in Program.cs
builder.Services.AddHostedService<DataSyncService>();
// Queue Interface
public interface IBackgroundTaskQueue
{
void Enqueue(EmailTask task);
Task<EmailTask> DequeueAsync(CancellationToken cancellationToken);
}
// Channel-based Implementation (thread-safe, high performance)
public class BackgroundTaskQueue : IBackgroundTaskQueue
{
private readonly Channel<EmailTask> _queue = Channel.CreateUnbounded<EmailTask>();
public void Enqueue(EmailTask task) => _queue.Writer.TryWrite(task);
public async Task<EmailTask> DequeueAsync(CancellationToken ct)
=> await _queue.Reader.ReadAsync(ct);
}
// Background Worker that processes the queue
public class EmailWorkerService : BackgroundService
{
private readonly IBackgroundTaskQueue _queue;
public EmailWorkerService(IBackgroundTaskQueue queue) { _queue = queue; }
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var task = await _queue.DequeueAsync(stoppingToken);
Console.WriteLine($"Sending email to: {task.To}");
// await _emailSender.SendAsync(task.To, task.Subject, task.Body);
}
}
}
Q1: Why use Channel<T> over ConcurrentQueue for background tasks?
Channel<T> provides async awaitable reading โ ReadAsync() awaits without spinning or polling. ConcurrentQueue requires manual polling loops with Thread.Sleep or Task.Delay, wasting CPU cycles.
Q2: How does graceful shutdown work in BackgroundService?
The host calls IHostedService.StopAsync() on SIGTERM/Ctrl+C, which triggers cancellation on the CancellationToken passed to ExecuteAsync(). Your service checks stoppingToken.IsCancellationRequested in its loop and exits cleanly.