Claude
Skill
dotnet-worker-services
Build long-running .NET background services with `BackgroundService`, Generic Host, graceful shutdown, configuration, logging, and deployment patterns suited to workers and daemons.
Virus-scanned
Reviewed automatically before listing.
Download
postpartum-genushyacinthus29-dotnet-skills-skills_dotnet-worker-services-bfa4ebd.zip · 12 KB
Install
skills CLI
npx skills add https://github.com/Postpartum-genushyacinthus29/dotnet-skills/tree/main/skills/dotnet-worker-services
Claude Code
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install postpartum-genushyacinthus29-dotnet-skills@llmmart
Git
git clone https://github.com/Postpartum-genushyacinthus29/dotnet-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole postpartum-genushyacinthus29/dotnet-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
.NET Worker Services
Trigger On
- building long-running background services or scheduled workers
- adding hosted services to an app or extracting them into a worker process
- reviewing graceful shutdown, cancellation, queue processing, or health behavior
Documentation
- Worker Services in .NET
- Background tasks with hosted services in ASP.NET Core
- Create Windows Service using BackgroundService
- App health checks in .NET
- Health checks in ASP.NET Core
References
- patterns.md - BackgroundService patterns, graceful shutdown, and health check implementations
- anti-patterns.md - Common worker service mistakes and how to avoid them
Workflow
Use BackgroundService as your base class:
- Provides standard
StartAsync/StopAsynchandling - Focus on implementing
ExecuteAsynconly - Proper cancellation token management built-in
- Provides standard
Handle scoped dependencies correctly:
- Create service scopes for scoped services
- No scope is created by default in hosted services
Implement graceful shutdown:
- Propagate cancellation tokens throughout
- Complete work promptly when token fires
- Avoid ungraceful shutdown at timeout
Keep execution loop thin:
- Move business logic to testable services
- Handle exceptions to prevent service crashes
- Use
PeriodicTimerfor scheduled work
Add observability:
- Use health checks for readiness/liveness
- Expose metrics and structured logging
- Consider distributed locks for multi-instance
Basic BackgroundService Pattern
Simple Worker
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Worker starting");
while (!stoppingToken.IsCancellationRequested)
{
try
{
_logger.LogInformation("Worker running at: {Time}", DateTimeOffset.Now);
await DoWorkAsync(stoppingToken);
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
// Graceful shutdown, not an error
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in worker iteration");
// Continue or break based on error severity
}
}
_logger.LogInformation("Worker stopping");
}
private async Task DoWorkAsync(CancellationToken cancellationToken)
{
// Business logic here
}
}
Using PeriodicTimer (Recommended)
public class TimedWorker : BackgroundService
{
private readonly ILogger<TimedWorker> _logger;
private readonly IServiceScopeFactory _scopeFactory;
private readonly TimeSpan _period = TimeSpan.FromMinutes(1);
public TimedWorker(ILogger<TimedWorker> logger, IServiceScopeFactory scopeFactory)
{
_logger = logger;
_scopeFactory = scopeFactory;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(_period);
while (await timer.WaitForNextTickAsync(stoppingToken))
{
try
{
await using var scope = _scopeFactory.CreateAsyncScope();
var processor = scope.ServiceProvider.GetRequiredService<IDataProcessor>();
await processor.ProcessAsync(stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing scheduled task");
}
}
}
}
Handling Scoped Dependencies
Correct Pattern with Scope Factory
public class ScopedWorker : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<ScopedWorker> _logger;
public ScopedWorker(IServiceScopeFactory scopeFactory, ILogger<ScopedWorker> logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// Create scope for each unit of work
await using var scope = _scopeFactory.CreateAsyncScope();
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var service = scope.ServiceProvider.GetRequiredService<IScopedService>();
await service.ProcessAsync(dbContext, stoppingToken);
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
}
}
}
Queue Processing Pattern
Message Queue Worker
public class QueueWorker : BackgroundService
{
private readonly ILogger<QueueWorker> _logger;
private readonly IServiceScopeFactory _scopeFactory;
private readonly IBackgroundTaskQueue _taskQueue;
public QueueWorker(
ILogger<QueueWorker> logger,
IServiceScopeFactory scopeFactory,
IBackgroundTaskQueue taskQueue)
{
_logger = logger;
_scopeFactory = scopeFactory;
_taskQueue = taskQueue;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Queue Worker started");
while (!stoppingToken.IsCancellationRequested)
{
var workItem = await _taskQueue.DequeueAsync(stoppingToken);
try
{
await using var scope = _scopeFactory.CreateAsyncScope();
await workItem(scope.ServiceProvider, stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing queued work item");
// Handle poison message - retry, dead-letter, etc.
}
}
}
}
// Task queue interface
public interface IBackgroundTaskQueue
{
ValueTask QueueBackgroundWorkItemAsync(
Func<IServiceProvider, CancellationToken, ValueTask> workItem);
ValueTask<Func<IServiceProvider, CancellationToken, ValueTask>> DequeueAsync(
CancellationToken cancellationToken);
}
Health Checks for Workers
Adding Health Check Endpoint
// Program.cs
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();
// Add health checks
builder.Services.AddHealthChecks()
.AddCheck<WorkerHealthCheck>("worker_health")
.AddResourceUtilizationHealthCheck();
// Add HTTP endpoint for health checks
builder.Services.AddHealthChecksUI();
// Or use simple TCP listener for Kubernetes
builder.Services.AddSingleton<TcpHealthProbeService>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<TcpHealthProbeService>());
var host = builder.Build();
host.Run();
Custom Health Check
public class WorkerHealthCheck : IHealthCheck
{
private readonly WorkerState _workerState;
public WorkerHealthCheck(WorkerState workerState)
{
_workerState = workerState;
}
public Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
if (_workerState.LastSuccessfulRun > DateTime.UtcNow.AddMinutes(-5))
{
return Task.FromResult(HealthCheckResult.Healthy(
$"Last successful run: {_workerState.LastSuccessfulRun}"));
}
return Task.FromResult(HealthCheckResult.Unhealthy(
$"No successful run since: {_workerState.LastSuccessfulRun}"));
}
}
// Shared state
public class WorkerState
{
public DateTime LastSuccessfulRun { get; set; } = DateTime.UtcNow;
public bool IsProcessing { get; set; }
}
Graceful Shutdown Pattern
Proper Shutdown Handling
public class GracefulWorker : BackgroundService
{
private readonly ILogger<GracefulWorker> _logger;
private int _currentWorkItemId;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Worker starting");
while (!stoppingToken.IsCancellationRequested)
{
_currentWorkItemId = GetNextWorkItemId();
try
{
// Pass cancellation token to all async operations
await ProcessWorkItemAsync(_currentWorkItemId, stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
_logger.LogInformation(
"Shutdown requested, stopping after work item {Id}", _currentWorkItemId);
break;
}
}
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Worker stopping gracefully");
await base.StopAsync(cancellationToken);
_logger.LogInformation("Worker stopped");
}
}
Windows Service Deployment
Configuring as Windows Service
// Program.cs
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddWindowsService(options =>
{
options.ServiceName = "My Worker Service";
});
builder.Services.AddHostedService<Worker>();
var host = builder.Build();
host.Run();
Project File Settings
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
</PropertyGroup>
</Project>
Best Practices
- Use BackgroundService as base class - Handles
StartAsync/StopAsyncboilerplate and cancellation management - Create scopes for scoped dependencies - Use
IServiceScopeFactoryto resolve scoped services like DbContext - Propagate cancellation tokens everywhere - Pass to all async methods for responsive shutdown
- Wrap work in try-catch - Unhandled exceptions stop the service completely
- Use PeriodicTimer for timed tasks - Cleaner than
Task.Delaywith proper cancellation support - Add health checks - Essential for Kubernetes liveness/readiness probes
- Avoid blocking StartAsync - Long initialization delays other hosted services
- Call base methods when overriding - Always call
await base.StartAsync()andawait base.StopAsync() - Publish as single file for Windows Service - Reduces deployment complexity and errors
- Consider scaling requirements - Separate worker projects if independent scaling is needed
Anti-Patterns to Avoid
| Anti-Pattern | Why It's Bad | Better Approach |
|---|---|---|
Ad-hoc while(true) loops |
No graceful shutdown, poor lifecycle | Use BackgroundService |
| Ignoring cancellation token | Ungraceful shutdown, resource leaks | Propagate token to all async calls |
| Injecting scoped services directly | Captive dependencies, memory leaks | Use IServiceScopeFactory |
Unhandled exceptions in ExecuteAsync |
Silently stops the worker | Wrap in try-catch, log, continue |
Long-running StartAsync |
Blocks other services from starting | Move work to ExecuteAsync |
async void methods |
Crashes process on exception | Use async Task |
| Missing health checks | No visibility into worker status | Implement IHealthCheck |
| Polling with tight loops | CPU waste, no responsiveness | Use PeriodicTimer or event-driven |
Not overriding StopAsync |
Missed cleanup opportunity | Override for graceful cleanup |
| Singleton DbContext | Not thread-safe, stale data | Create scopes per operation |
Deliver
- well-behaved worker processes and hosted services
- predictable startup and shutdown behavior
- proper scoped dependency handling
- health checks for production observability
- retry and poison-message handling for queue work
Validate
- cancellation token propagated and shutdown honored
- scoped services resolved within proper scopes
- exception handling prevents service crashes
- health checks report accurate worker status
- runtime behavior visible through logs or telemetry
- no blocking calls in async context
Files (dotnet-skills)
-
references
-
anti-patterns.md 22.7 KB
# Worker Services Anti-Patterns Reference This reference documents common mistakes when building .NET worker services and provides corrective guidance. --- ## Lifecycle and Initialization Anti-Patterns ### 1. Blocking in StartAsync **Problem:** Long-running operations in `StartAsync` block other hosted services from starting. ```csharp // BAD: Blocks host startup public class BadWorker : BackgroundService { public override async Task StartAsync(CancellationToken cancellationToken) { // This blocks all other hosted services from starting await LoadLargeDatasetAsync(); await WarmupCachesAsync(); await base.StartAsync(cancellationToken); } } ``` **Correction:** Move initialization to `ExecuteAsync`: ```csharp // GOOD: Non-blocking startup public class GoodWorker : BackgroundService { private bool _initialized; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // Initialize in ExecuteAsync, not StartAsync await LoadLargeDatasetAsync(stoppingToken); await WarmupCachesAsync(stoppingToken); _initialized = true; while (!stoppingToken.IsCancellationRequested) { await DoWorkAsync(stoppingToken); } } } ``` ### 2. Forgetting to Call Base Methods **Problem:** Not calling base class methods breaks lifecycle management. ```csharp // BAD: Missing base.StartAsync public class BadWorker : BackgroundService { public override async Task StartAsync(CancellationToken cancellationToken) { await InitializeAsync(); // Missing: await base.StartAsync(cancellationToken); } public override async Task StopAsync(CancellationToken cancellationToken) { await CleanupAsync(); // Missing: await base.StopAsync(cancellationToken); } } ``` **Correction:** Always call base methods: ```csharp // GOOD: Proper base method calls public class GoodWorker : BackgroundService { public override async Task StartAsync(CancellationToken cancellationToken) { await InitializeAsync(); await base.StartAsync(cancellationToken); } public override async Task StopAsync(CancellationToken cancellationToken) { await CleanupAsync(); await base.StopAsync(cancellationToken); } } ``` ### 3. Using async void **Problem:** `async void` methods crash the process on unhandled exceptions. ```csharp // BAD: async void crashes process public class BadWorker : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { ProcessItemAsync(); // Fire and forget } } private async void ProcessItemAsync() // DANGEROUS { await DoWorkThatMightFailAsync(); // Unhandled exception = process crash } } ``` **Correction:** Use `async Task` and handle exceptions: ```csharp // GOOD: async Task with proper handling public class GoodWorker : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { await ProcessItemAsync(stoppingToken); } } private async Task ProcessItemAsync(CancellationToken cancellationToken) { try { await DoWorkThatMightFailAsync(cancellationToken); } catch (Exception ex) { _logger.LogError(ex, "Error processing item"); } } } ``` --- ## Cancellation and Shutdown Anti-Patterns ### 4. Ignoring Cancellation Token **Problem:** Not propagating cancellation tokens causes ungraceful shutdown. ```csharp // BAD: Ignoring cancellation public class BadWorker : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (true) // Never checks stoppingToken { await ProcessAsync(); // No cancellation token passed await Task.Delay(5000); // No cancellation token } } } ``` **Correction:** Propagate cancellation everywhere: ```csharp // GOOD: Proper cancellation public class GoodWorker : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { try { await ProcessAsync(stoppingToken); await Task.Delay(5000, stoppingToken); } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { break; // Graceful exit } } } } ``` ### 5. Swallowing OperationCanceledException **Problem:** Catching and ignoring `OperationCanceledException` during shutdown hides issues. ```csharp // BAD: Swallowing cancellation public class BadWorker : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { try { await DoWorkAsync(stoppingToken); } catch (Exception ex) // Catches OperationCanceledException too { _logger.LogError(ex, "Error"); // Logs cancellation as error // Continues loop even on shutdown } } } } ``` **Correction:** Handle cancellation separately: ```csharp // GOOD: Proper exception handling public class GoodWorker : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { try { await DoWorkAsync(stoppingToken); } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { _logger.LogInformation("Shutdown requested"); break; } catch (Exception ex) { _logger.LogError(ex, "Error during work"); } } } } ``` ### 6. No Shutdown Timeout Configuration **Problem:** Default 30-second shutdown timeout may be too short for draining work. ```csharp // BAD: Relying on default timeout var builder = Host.CreateApplicationBuilder(args); builder.Services.AddHostedService<LongRunningWorker>(); // Default ShutdownTimeout is 30 seconds ``` **Correction:** Configure appropriate timeout: ```csharp // GOOD: Configured shutdown timeout var builder = Host.CreateApplicationBuilder(args); builder.Services.Configure<HostOptions>(options => { options.ShutdownTimeout = TimeSpan.FromMinutes(2); }); builder.Services.AddHostedService<LongRunningWorker>(); ``` --- ## Dependency Injection Anti-Patterns ### 7. Injecting Scoped Services Directly **Problem:** Injecting scoped services into singleton `BackgroundService` creates captive dependencies. ```csharp // BAD: Captive dependency public class BadWorker : BackgroundService { private readonly AppDbContext _dbContext; // Singleton captures scoped service public BadWorker(AppDbContext dbContext) // Direct injection { _dbContext = dbContext; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { // DbContext is never refreshed, connection may be stale var items = await _dbContext.Items.ToListAsync(stoppingToken); } } } ``` **Correction:** Use `IServiceScopeFactory`: ```csharp // GOOD: Proper scoped service handling public class GoodWorker : BackgroundService { private readonly IServiceScopeFactory _scopeFactory; public GoodWorker(IServiceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { await using var scope = _scopeFactory.CreateAsyncScope(); var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>(); var items = await dbContext.Items.ToListAsync(stoppingToken); } } } ``` ### 8. Singleton DbContext **Problem:** Using `DbContext` as singleton causes thread-safety issues and stale data. ```csharp // BAD: Singleton DbContext registration builder.Services.AddSingleton<AppDbContext>(); // Thread-unsafe, stale tracking builder.Services.AddHostedService<Worker>(); ``` **Correction:** Register as scoped and use scope factory: ```csharp // GOOD: Scoped DbContext builder.Services.AddDbContext<AppDbContext>(); builder.Services.AddHostedService<Worker>(); // In worker, create scopes for each unit of work ``` ### 9. Service Locator in Constructor **Problem:** Resolving services in constructor can fail if service isn't registered. ```csharp // BAD: Service resolution in constructor public class BadWorker : BackgroundService { private readonly ISpecialService _service; public BadWorker(IServiceProvider provider) { _service = provider.GetRequiredService<ISpecialService>(); // May throw } } ``` **Correction:** Use proper constructor injection or scope factory: ```csharp // GOOD: Explicit dependency public class GoodWorker : BackgroundService { private readonly ISpecialService _service; public GoodWorker(ISpecialService service) // Fails at startup if not registered { _service = service; } } ``` --- ## Loop and Timing Anti-Patterns ### 10. Tight Polling Loop **Problem:** Polling without delay wastes CPU and may overload resources. ```csharp // BAD: CPU-burning tight loop public class BadWorker : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { var hasWork = await CheckForWorkAsync(); if (hasWork) { await ProcessAsync(); } // No delay - burns CPU when idle } } } ``` **Correction:** Add appropriate delays or use event-driven patterns: ```csharp // GOOD: Appropriate delay public class GoodWorker : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { var hasWork = await CheckForWorkAsync(stoppingToken); if (hasWork) { await ProcessAsync(stoppingToken); } else { await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken); } } } } ``` ### 11. Task.Delay Instead of PeriodicTimer **Problem:** `Task.Delay` doesn't account for processing time, causing drift. ```csharp // BAD: Interval drift public class BadWorker : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { await ProcessAsync(); // Takes variable time await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); // Actual interval is 5 minutes + processing time } } } ``` **Correction:** Use `PeriodicTimer` for consistent intervals: ```csharp // GOOD: Consistent intervals with PeriodicTimer public class GoodWorker : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5)); // Run immediately, then on schedule await ProcessAsync(stoppingToken); while (await timer.WaitForNextTickAsync(stoppingToken)) { await ProcessAsync(stoppingToken); } } } ``` ### 12. Ad-hoc while(true) Without BackgroundService **Problem:** Raw `while(true)` loops lack proper lifecycle management. ```csharp // BAD: Manual loop without BackgroundService public class BadService : IHostedService { private Task? _executingTask; public Task StartAsync(CancellationToken cancellationToken) { _executingTask = Task.Run(async () => { while (true) // No way to stop { await DoWorkAsync(); await Task.Delay(5000); } }); return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) { // Cannot stop the loop return Task.CompletedTask; } } ``` **Correction:** Use `BackgroundService`: ```csharp // GOOD: BackgroundService with proper lifecycle public class GoodService : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { await DoWorkAsync(stoppingToken); await Task.Delay(5000, stoppingToken); } } } ``` --- ## Exception Handling Anti-Patterns ### 13. Unhandled Exceptions in ExecuteAsync **Problem:** Unhandled exceptions silently stop the worker without notification. ```csharp // BAD: No exception handling public class BadWorker : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { await ProcessAsync(stoppingToken); // Exception stops worker silently } } } ``` **Correction:** Wrap in try-catch with logging: ```csharp // GOOD: Exception handling with logging public class GoodWorker : BackgroundService { private readonly ILogger<GoodWorker> _logger; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { try { await ProcessAsync(stoppingToken); } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { break; } catch (Exception ex) { _logger.LogError(ex, "Error in worker iteration"); await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); // Backoff } } } } ``` ### 14. Retrying Forever Without Backoff **Problem:** Immediate retry on failure can overwhelm resources. ```csharp // BAD: No backoff public class BadWorker : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { try { await ConnectAndProcessAsync(stoppingToken); } catch (Exception) { // Immediately retry - can overwhelm network/service } } } } ``` **Correction:** Implement exponential backoff: ```csharp // GOOD: Exponential backoff public class GoodWorker : BackgroundService { private readonly ILogger<GoodWorker> _logger; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var backoff = TimeSpan.FromSeconds(1); var maxBackoff = TimeSpan.FromMinutes(5); while (!stoppingToken.IsCancellationRequested) { try { await ConnectAndProcessAsync(stoppingToken); backoff = TimeSpan.FromSeconds(1); // Reset on success } catch (Exception ex) { _logger.LogWarning(ex, "Connection failed, retrying in {Delay}", backoff); await Task.Delay(backoff, stoppingToken); backoff = TimeSpan.FromTicks(Math.Min(backoff.Ticks * 2, maxBackoff.Ticks)); } } } } ``` --- ## Observability Anti-Patterns ### 15. No Health Checks **Problem:** Workers without health checks are invisible to orchestrators like Kubernetes. ```csharp // BAD: No health visibility var builder = Host.CreateApplicationBuilder(args); builder.Services.AddHostedService<Worker>(); var host = builder.Build(); host.Run(); ``` **Correction:** Add health checks: ```csharp // GOOD: Health check integration var builder = Host.CreateApplicationBuilder(args); builder.Services.AddSingleton<WorkerState>(); builder.Services.AddHostedService<Worker>(); builder.Services.AddHealthChecks() .AddCheck<WorkerHealthCheck>("worker"); builder.Services.AddHostedService<TcpHealthProbeService>(); var host = builder.Build(); host.Run(); ``` ### 16. Console.WriteLine Instead of Structured Logging **Problem:** `Console.WriteLine` loses structure and is hard to aggregate. ```csharp // BAD: Unstructured output public class BadWorker : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { Console.WriteLine("Worker started at " + DateTime.Now); while (!stoppingToken.IsCancellationRequested) { Console.WriteLine("Processing item " + itemId); } } } ``` **Correction:** Use `ILogger` with structured data: ```csharp // GOOD: Structured logging public class GoodWorker : BackgroundService { private readonly ILogger<GoodWorker> _logger; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("Worker started at {StartTime}", DateTime.UtcNow); while (!stoppingToken.IsCancellationRequested) { _logger.LogInformation("Processing item {ItemId}", itemId); } } } ``` ### 17. Missing Correlation IDs **Problem:** Log entries without correlation IDs are hard to trace across operations. ```csharp // BAD: No correlation public class BadWorker : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { var items = await FetchItemsAsync(); foreach (var item in items) { _logger.LogInformation("Processing item"); // Which item? await ProcessAsync(item); } } } } ``` **Correction:** Add correlation/operation context: ```csharp // GOOD: Correlation and context public class GoodWorker : BackgroundService { private readonly ILogger<GoodWorker> _logger; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { var batchId = Guid.NewGuid().ToString("N")[..8]; using var scope = _logger.BeginScope(new { BatchId = batchId }); var items = await FetchItemsAsync(stoppingToken); _logger.LogInformation("Fetched {Count} items", items.Count); foreach (var item in items) { using var itemScope = _logger.BeginScope(new { ItemId = item.Id }); _logger.LogInformation("Processing item"); await ProcessAsync(item, stoppingToken); } } } } ``` --- ## Concurrency Anti-Patterns ### 18. Shared Mutable State Without Synchronization **Problem:** Multiple workers accessing shared state causes race conditions. ```csharp // BAD: Unsynchronized shared state public class BadWorker : BackgroundService { private int _processedCount; // Shared mutable state protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var tasks = Enumerable.Range(0, 4) .Select(_ => ProcessLoopAsync(stoppingToken)); await Task.WhenAll(tasks); } private async Task ProcessLoopAsync(CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) { await ProcessAsync(); _processedCount++; // Race condition } } } ``` **Correction:** Use thread-safe operations: ```csharp // GOOD: Thread-safe counter public class GoodWorker : BackgroundService { private int _processedCount; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var tasks = Enumerable.Range(0, 4) .Select(_ => ProcessLoopAsync(stoppingToken)); await Task.WhenAll(tasks); } private async Task ProcessLoopAsync(CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) { await ProcessAsync(cancellationToken); Interlocked.Increment(ref _processedCount); } } } ``` ### 19. Unbounded Parallelism **Problem:** Processing items without limiting concurrency can exhaust resources. ```csharp // BAD: Unbounded parallelism public class BadWorker : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var items = await GetItemsAsync(); var tasks = items.Select(item => ProcessAsync(item)); // Starts ALL at once await Task.WhenAll(tasks); // May exhaust connections, memory } } ``` **Correction:** Use bounded parallelism: ```csharp // GOOD: Bounded parallelism public class GoodWorker : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var items = await GetItemsAsync(stoppingToken); await Parallel.ForEachAsync( items, new ParallelOptions { MaxDegreeOfParallelism = 4, CancellationToken = stoppingToken }, async (item, ct) => await ProcessAsync(item, ct)); } } ``` ### 20. No Distributed Locking for Multi-Instance **Problem:** Multiple worker instances process the same items without coordination. ```csharp // BAD: No coordination between instances public class BadWorker : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { // Multiple instances may process same items var items = await FetchPendingItemsAsync(); foreach (var item in items) { await ProcessAsync(item); } } } } ``` **Correction:** Implement distributed locking or partitioning: ```csharp // GOOD: Distributed lock public class GoodWorker : BackgroundService { private readonly IDistributedLockProvider _lockProvider; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { var items = await FetchPendingItemsAsync(stoppingToken); foreach (var item in items) { await using var @lock = await _lockProvider.TryAcquireLockAsync( $"item:{item.Id}", TimeSpan.FromMinutes(5), stoppingToken); if (@lock != null) { await ProcessAsync(item, stoppingToken); } } } } } ``` -
patterns.md 21.2 KB
# Worker Services Patterns Reference This reference covers BackgroundService patterns, graceful shutdown implementation, and health check patterns for .NET worker services. ## BackgroundService Patterns ### 1. Basic Worker Pattern The simplest pattern for periodic work: ```csharp public class BasicWorker : BackgroundService { private readonly ILogger<BasicWorker> _logger; public BasicWorker(ILogger<BasicWorker> logger) => _logger = logger; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { _logger.LogInformation("Processing at {Time}", DateTimeOffset.Now); await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken); } } } ``` ### 2. Scoped Dependency Pattern Create a new scope for each unit of work to properly handle scoped dependencies like DbContext: ```csharp public class ScopedDependencyWorker : BackgroundService { private readonly IServiceScopeFactory _scopeFactory; private readonly ILogger<ScopedDependencyWorker> _logger; public ScopedDependencyWorker(IServiceScopeFactory scopeFactory, ILogger<ScopedDependencyWorker> logger) { _scopeFactory = scopeFactory; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { await using var scope = _scopeFactory.CreateAsyncScope(); var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>(); var processor = scope.ServiceProvider.GetRequiredService<IDataProcessor>(); await processor.ProcessBatchAsync(dbContext, stoppingToken); await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); } } } ``` ### 3. PeriodicTimer Pattern Preferred over `Task.Delay` for scheduled work with cleaner cancellation semantics: ```csharp public class PeriodicTimerWorker : BackgroundService { private readonly TimeSpan _interval = TimeSpan.FromMinutes(5); private readonly IServiceScopeFactory _scopeFactory; private readonly ILogger<PeriodicTimerWorker> _logger; public PeriodicTimerWorker(IServiceScopeFactory scopeFactory, ILogger<PeriodicTimerWorker> logger) { _scopeFactory = scopeFactory; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // Run immediately on startup await RunIterationAsync(stoppingToken); using var timer = new PeriodicTimer(_interval); while (await timer.WaitForNextTickAsync(stoppingToken)) { await RunIterationAsync(stoppingToken); } } private async Task RunIterationAsync(CancellationToken cancellationToken) { try { await using var scope = _scopeFactory.CreateAsyncScope(); var service = scope.ServiceProvider.GetRequiredService<IScheduledService>(); await service.ExecuteAsync(cancellationToken); } catch (Exception ex) { _logger.LogError(ex, "Error in scheduled iteration"); } } } ``` ### 4. Queue Consumer Pattern Process items from a queue with proper backpressure and error handling: ```csharp public class QueueConsumerWorker : BackgroundService { private readonly IBackgroundTaskQueue _queue; private readonly IServiceScopeFactory _scopeFactory; private readonly ILogger<QueueConsumerWorker> _logger; public QueueConsumerWorker( IBackgroundTaskQueue queue, IServiceScopeFactory scopeFactory, ILogger<QueueConsumerWorker> logger) { _queue = queue; _scopeFactory = scopeFactory; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("Queue Consumer starting"); await foreach (var workItem in _queue.DequeueAllAsync(stoppingToken)) { try { await using var scope = _scopeFactory.CreateAsyncScope(); await workItem.ExecuteAsync(scope.ServiceProvider, stoppingToken); } catch (Exception ex) { _logger.LogError(ex, "Error processing work item {Id}", workItem.Id); await HandleFailedItemAsync(workItem, ex); } } } private Task HandleFailedItemAsync(IWorkItem workItem, Exception exception) { // Implement retry, dead-letter, or alerting logic return Task.CompletedTask; } } ``` ### 5. Parallel Worker Pattern Process multiple items concurrently with controlled parallelism: ```csharp public class ParallelWorker : BackgroundService { private readonly IServiceScopeFactory _scopeFactory; private readonly ILogger<ParallelWorker> _logger; private readonly int _maxDegreeOfParallelism = Environment.ProcessorCount; public ParallelWorker(IServiceScopeFactory scopeFactory, ILogger<ParallelWorker> logger) { _scopeFactory = scopeFactory; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var channel = Channel.CreateBounded<WorkItem>(new BoundedChannelOptions(100) { FullMode = BoundedChannelFullMode.Wait }); // Start producer var producer = ProduceItemsAsync(channel.Writer, stoppingToken); // Start consumers var consumers = Enumerable.Range(0, _maxDegreeOfParallelism) .Select(_ => ConsumeItemsAsync(channel.Reader, stoppingToken)) .ToArray(); await Task.WhenAll(consumers.Prepend(producer)); } private async Task ProduceItemsAsync(ChannelWriter<WorkItem> writer, CancellationToken cancellationToken) { try { while (!cancellationToken.IsCancellationRequested) { await using var scope = _scopeFactory.CreateAsyncScope(); var source = scope.ServiceProvider.GetRequiredService<IWorkItemSource>(); await foreach (var item in source.GetItemsAsync(cancellationToken)) { await writer.WriteAsync(item, cancellationToken); } await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken); } } finally { writer.Complete(); } } private async Task ConsumeItemsAsync(ChannelReader<WorkItem> reader, CancellationToken cancellationToken) { await foreach (var item in reader.ReadAllAsync(cancellationToken)) { try { await using var scope = _scopeFactory.CreateAsyncScope(); var processor = scope.ServiceProvider.GetRequiredService<IWorkItemProcessor>(); await processor.ProcessAsync(item, cancellationToken); } catch (Exception ex) { _logger.LogError(ex, "Error processing item {Id}", item.Id); } } } } ``` ### 6. Event-Driven Worker Pattern React to external events instead of polling: ```csharp public class EventDrivenWorker : BackgroundService { private readonly IMessageSubscriber _subscriber; private readonly IServiceScopeFactory _scopeFactory; private readonly ILogger<EventDrivenWorker> _logger; public EventDrivenWorker( IMessageSubscriber subscriber, IServiceScopeFactory scopeFactory, ILogger<EventDrivenWorker> logger) { _subscriber = subscriber; _scopeFactory = scopeFactory; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await _subscriber.SubscribeAsync<OrderCreatedEvent>( async (message, ct) => { await using var scope = _scopeFactory.CreateAsyncScope(); var handler = scope.ServiceProvider.GetRequiredService<IOrderHandler>(); await handler.HandleAsync(message, ct); }, stoppingToken); // Keep alive until cancellation await Task.Delay(Timeout.Infinite, stoppingToken); } } ``` --- ## Graceful Shutdown Patterns ### 1. Basic Graceful Shutdown Respond to cancellation token and complete current work: ```csharp public class GracefulShutdownWorker : BackgroundService { private readonly ILogger<GracefulShutdownWorker> _logger; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("Worker started"); while (!stoppingToken.IsCancellationRequested) { try { await ProcessCurrentBatchAsync(stoppingToken); await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { _logger.LogInformation("Shutdown requested, completing gracefully"); break; } } _logger.LogInformation("Worker completed shutdown"); } } ``` ### 2. StopAsync Override for Cleanup Override StopAsync for explicit cleanup operations: ```csharp public class CleanupWorker : BackgroundService { private readonly ILogger<CleanupWorker> _logger; private readonly IExternalConnection _connection; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await _connection.ConnectAsync(stoppingToken); while (!stoppingToken.IsCancellationRequested) { await _connection.ProcessMessagesAsync(stoppingToken); } } public override async Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("Stopping worker, disconnecting..."); try { await _connection.DisconnectAsync(cancellationToken); _logger.LogInformation("Disconnected cleanly"); } catch (Exception ex) { _logger.LogWarning(ex, "Error during disconnect, may have leaked resources"); } await base.StopAsync(cancellationToken); } } ``` ### 3. Work-in-Progress Tracking Track ongoing work to ensure completion before shutdown: ```csharp public class TrackedWorkWorker : BackgroundService { private readonly ILogger<TrackedWorkWorker> _logger; private readonly SemaphoreSlim _workTracker = new(0, int.MaxValue); private int _activeWorkItems; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var channel = Channel.CreateUnbounded<WorkItem>(); // Start processor var processor = ProcessItemsAsync(channel.Reader, stoppingToken); // Produce items until cancelled while (!stoppingToken.IsCancellationRequested) { var items = await FetchItemsAsync(stoppingToken); foreach (var item in items) { Interlocked.Increment(ref _activeWorkItems); await channel.Writer.WriteAsync(item, stoppingToken); } await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); } channel.Writer.Complete(); await processor; } private async Task ProcessItemsAsync(ChannelReader<WorkItem> reader, CancellationToken cancellationToken) { await foreach (var item in reader.ReadAllAsync(cancellationToken)) { try { await ProcessItemAsync(item, cancellationToken); } finally { Interlocked.Decrement(ref _activeWorkItems); _workTracker.Release(); } } } public override async Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("Waiting for {Count} work items to complete", _activeWorkItems); // Wait for all work to complete or timeout var timeout = Task.Delay(TimeSpan.FromSeconds(30), cancellationToken); while (_activeWorkItems > 0 && !timeout.IsCompleted) { await Task.WhenAny(_workTracker.WaitAsync(cancellationToken), timeout); } if (_activeWorkItems > 0) { _logger.LogWarning("Shutdown with {Count} items still processing", _activeWorkItems); } await base.StopAsync(cancellationToken); } } ``` ### 4. Configurable Shutdown Timeout Configure host shutdown timeout for long-running operations: ```csharp // Program.cs var builder = Host.CreateApplicationBuilder(args); builder.Services.Configure<HostOptions>(options => { options.ShutdownTimeout = TimeSpan.FromSeconds(60); }); builder.Services.AddHostedService<LongRunningWorker>(); ``` ### 5. Two-Phase Shutdown Implement soft and hard shutdown phases: ```csharp public class TwoPhaseShutdownWorker : BackgroundService { private readonly ILogger<TwoPhaseShutdownWorker> _logger; private volatile bool _softShutdownRequested; private readonly CancellationTokenSource _internalCts = new(); protected override async Task ExecuteAsync(CancellationToken stoppingToken) { using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource( stoppingToken, _internalCts.Token); stoppingToken.Register(() => _softShutdownRequested = true); while (!linkedCts.Token.IsCancellationRequested) { if (_softShutdownRequested) { _logger.LogInformation("Soft shutdown: finishing current batch"); // Complete current iteration but don't start new work } await ProcessBatchAsync(linkedCts.Token); } } public override async Task StopAsync(CancellationToken cancellationToken) { // Phase 1: Soft shutdown (stop accepting new work) _softShutdownRequested = true; // Give time for graceful completion await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken); // Phase 2: Hard shutdown (cancel internal operations) _internalCts.Cancel(); await base.StopAsync(cancellationToken); } } ``` --- ## Health Check Patterns ### 1. Basic Worker Health Check Track last successful execution time: ```csharp public class WorkerHealthCheck : IHealthCheck { private readonly WorkerState _state; private readonly TimeSpan _maxAge = TimeSpan.FromMinutes(5); public WorkerHealthCheck(WorkerState state) => _state = state; public Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default) { var timeSinceLastRun = DateTime.UtcNow - _state.LastSuccessfulRun; if (timeSinceLastRun > _maxAge) { return Task.FromResult(HealthCheckResult.Unhealthy( $"No successful run in {timeSinceLastRun.TotalMinutes:F1} minutes")); } return Task.FromResult(HealthCheckResult.Healthy( $"Last run: {_state.LastSuccessfulRun:O}")); } } public class WorkerState { public DateTime LastSuccessfulRun { get; set; } = DateTime.UtcNow; public int ConsecutiveFailures { get; set; } public bool IsProcessing { get; set; } } ``` ### 2. Liveness vs Readiness Health Checks Separate liveness (is the process alive) from readiness (can it accept work): ```csharp // Liveness: Is the worker process running and not deadlocked? public class WorkerLivenessCheck : IHealthCheck { private readonly WorkerState _state; private readonly TimeSpan _maxProcessingTime = TimeSpan.FromMinutes(10); public Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default) { if (_state.IsProcessing && _state.ProcessingStarted < DateTime.UtcNow - _maxProcessingTime) { return Task.FromResult(HealthCheckResult.Unhealthy( "Worker appears stuck - processing for too long")); } return Task.FromResult(HealthCheckResult.Healthy()); } } // Readiness: Is the worker ready to handle new work? public class WorkerReadinessCheck : IHealthCheck { private readonly WorkerState _state; private readonly IDbConnectionFactory _dbFactory; public async Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default) { // Check dependencies are available try { await using var connection = await _dbFactory.CreateConnectionAsync(cancellationToken); await connection.OpenAsync(cancellationToken); } catch (Exception ex) { return HealthCheckResult.Unhealthy("Database unavailable", ex); } if (_state.ConsecutiveFailures > 5) { return HealthCheckResult.Degraded("Multiple consecutive failures"); } return HealthCheckResult.Healthy(); } } ``` ### 3. Health Check Registration ```csharp // Program.cs var builder = Host.CreateApplicationBuilder(args); builder.Services.AddSingleton<WorkerState>(); builder.Services.AddHostedService<Worker>(); builder.Services.AddHealthChecks() .AddCheck<WorkerLivenessCheck>("liveness", tags: ["live"]) .AddCheck<WorkerReadinessCheck>("readiness", tags: ["ready"]) .AddCheck<WorkerHealthCheck>("worker", tags: ["worker"]); // For workers without ASP.NET Core, expose via TCP builder.Services.AddHostedService<TcpHealthProbeService>(); ``` ### 4. TCP Health Probe for Kubernetes Expose health without HTTP for pure workers: ```csharp public class TcpHealthProbeService : BackgroundService { private readonly HealthCheckService _healthCheckService; private readonly ILogger<TcpHealthProbeService> _logger; private readonly int _port; public TcpHealthProbeService( HealthCheckService healthCheckService, ILogger<TcpHealthProbeService> logger, IConfiguration configuration) { _healthCheckService = healthCheckService; _logger = logger; _port = configuration.GetValue<int>("HealthProbe:TcpPort", 8080); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var listener = new TcpListener(IPAddress.Any, _port); listener.Start(); _logger.LogInformation("Health probe listening on port {Port}", _port); while (!stoppingToken.IsCancellationRequested) { var client = await listener.AcceptTcpClientAsync(stoppingToken); _ = HandleClientAsync(client, stoppingToken); } listener.Stop(); } private async Task HandleClientAsync(TcpClient client, CancellationToken cancellationToken) { using (client) { var result = await _healthCheckService.CheckHealthAsync(cancellationToken); var response = result.Status == HealthStatus.Healthy ? "HTTP/1.1 200 OK\r\n\r\n" : "HTTP/1.1 503 Service Unavailable\r\n\r\n"; var bytes = Encoding.UTF8.GetBytes(response); await client.GetStream().WriteAsync(bytes, cancellationToken); } } } ``` ### 5. Startup Health Check Report unhealthy during initialization: ```csharp public class StartupHealthCheck : IHealthCheck { private volatile bool _isReady; public bool IsReady { get => _isReady; set => _isReady = value; } public Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default) { if (_isReady) { return Task.FromResult(HealthCheckResult.Healthy("Startup complete")); } return Task.FromResult(HealthCheckResult.Unhealthy("Still starting up")); } } public class InitializingWorker : BackgroundService { private readonly StartupHealthCheck _startupCheck; private readonly ILogger<InitializingWorker> _logger; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("Initializing..."); await InitializeAsync(stoppingToken); _startupCheck.IsReady = true; _logger.LogInformation("Initialization complete, starting work"); while (!stoppingToken.IsCancellationRequested) { await DoWorkAsync(stoppingToken); } } } ``` ### 6. Circuit Breaker Health Check Track error rates and circuit state: ```csharp public class CircuitBreakerHealthCheck : IHealthCheck { private readonly CircuitBreakerState _state; public Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default) { return Task.FromResult(_state.State switch { CircuitState.Closed => HealthCheckResult.Healthy("Circuit closed"), CircuitState.HalfOpen => HealthCheckResult.Degraded("Circuit half-open, testing recovery"), CircuitState.Open => HealthCheckResult.Unhealthy($"Circuit open until {_state.ResetTime:O}"), _ => HealthCheckResult.Unhealthy("Unknown circuit state") }); } } public class CircuitBreakerState { public CircuitState State { get; set; } = CircuitState.Closed; public DateTime ResetTime { get; set; } public int FailureCount { get; set; } } public enum CircuitState { Closed, Open, HalfOpen } ```
-
-
SKILL.md 13 KB
--- name: dotnet-worker-services version: "1.0.0" category: "Distributed" description: "Build long-running .NET background services with `BackgroundService`, Generic Host, graceful shutdown, configuration, logging, and deployment patterns suited to workers and daemons." compatibility: "Requires a worker, hosted service, or background-processing scenario." --- # .NET Worker Services ## Trigger On - building long-running background services or scheduled workers - adding hosted services to an app or extracting them into a worker process - reviewing graceful shutdown, cancellation, queue processing, or health behavior ## Documentation - [Worker Services in .NET](https://learn.microsoft.com/en-us/dotnet/core/extensions/workers) - [Background tasks with hosted services in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-10.0) - [Create Windows Service using BackgroundService](https://learn.microsoft.com/en-us/dotnet/core/extensions/windows-service) - [App health checks in .NET](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/diagnostic-health-checks) - [Health checks in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks?view=aspnetcore-10.0) ### References - [patterns.md](references/patterns.md) - BackgroundService patterns, graceful shutdown, and health check implementations - [anti-patterns.md](references/anti-patterns.md) - Common worker service mistakes and how to avoid them ## Workflow 1. **Use BackgroundService as your base class:** - Provides standard `StartAsync`/`StopAsync` handling - Focus on implementing `ExecuteAsync` only - Proper cancellation token management built-in 2. **Handle scoped dependencies correctly:** - Create service scopes for scoped services - No scope is created by default in hosted services 3. **Implement graceful shutdown:** - Propagate cancellation tokens throughout - Complete work promptly when token fires - Avoid ungraceful shutdown at timeout 4. **Keep execution loop thin:** - Move business logic to testable services - Handle exceptions to prevent service crashes - Use `PeriodicTimer` for scheduled work 5. **Add observability:** - Use health checks for readiness/liveness - Expose metrics and structured logging - Consider distributed locks for multi-instance ## Basic BackgroundService Pattern ### Simple Worker ```csharp public class Worker : BackgroundService { private readonly ILogger<Worker> _logger; public Worker(ILogger<Worker> logger) { _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("Worker starting"); while (!stoppingToken.IsCancellationRequested) { try { _logger.LogInformation("Worker running at: {Time}", DateTimeOffset.Now); await DoWorkAsync(stoppingToken); await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { // Graceful shutdown, not an error break; } catch (Exception ex) { _logger.LogError(ex, "Error in worker iteration"); // Continue or break based on error severity } } _logger.LogInformation("Worker stopping"); } private async Task DoWorkAsync(CancellationToken cancellationToken) { // Business logic here } } ``` ### Using PeriodicTimer (Recommended) ```csharp public class TimedWorker : BackgroundService { private readonly ILogger<TimedWorker> _logger; private readonly IServiceScopeFactory _scopeFactory; private readonly TimeSpan _period = TimeSpan.FromMinutes(1); public TimedWorker(ILogger<TimedWorker> logger, IServiceScopeFactory scopeFactory) { _logger = logger; _scopeFactory = scopeFactory; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { using var timer = new PeriodicTimer(_period); while (await timer.WaitForNextTickAsync(stoppingToken)) { try { await using var scope = _scopeFactory.CreateAsyncScope(); var processor = scope.ServiceProvider.GetRequiredService<IDataProcessor>(); await processor.ProcessAsync(stoppingToken); } catch (Exception ex) { _logger.LogError(ex, "Error processing scheduled task"); } } } } ``` ## Handling Scoped Dependencies ### Correct Pattern with Scope Factory ```csharp public class ScopedWorker : BackgroundService { private readonly IServiceScopeFactory _scopeFactory; private readonly ILogger<ScopedWorker> _logger; public ScopedWorker(IServiceScopeFactory scopeFactory, ILogger<ScopedWorker> logger) { _scopeFactory = scopeFactory; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { // Create scope for each unit of work await using var scope = _scopeFactory.CreateAsyncScope(); var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>(); var service = scope.ServiceProvider.GetRequiredService<IScopedService>(); await service.ProcessAsync(dbContext, stoppingToken); await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); } } } ``` ## Queue Processing Pattern ### Message Queue Worker ```csharp public class QueueWorker : BackgroundService { private readonly ILogger<QueueWorker> _logger; private readonly IServiceScopeFactory _scopeFactory; private readonly IBackgroundTaskQueue _taskQueue; public QueueWorker( ILogger<QueueWorker> logger, IServiceScopeFactory scopeFactory, IBackgroundTaskQueue taskQueue) { _logger = logger; _scopeFactory = scopeFactory; _taskQueue = taskQueue; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("Queue Worker started"); while (!stoppingToken.IsCancellationRequested) { var workItem = await _taskQueue.DequeueAsync(stoppingToken); try { await using var scope = _scopeFactory.CreateAsyncScope(); await workItem(scope.ServiceProvider, stoppingToken); } catch (Exception ex) { _logger.LogError(ex, "Error processing queued work item"); // Handle poison message - retry, dead-letter, etc. } } } } // Task queue interface public interface IBackgroundTaskQueue { ValueTask QueueBackgroundWorkItemAsync( Func<IServiceProvider, CancellationToken, ValueTask> workItem); ValueTask<Func<IServiceProvider, CancellationToken, ValueTask>> DequeueAsync( CancellationToken cancellationToken); } ``` ## Health Checks for Workers ### Adding Health Check Endpoint ```csharp // Program.cs var builder = Host.CreateApplicationBuilder(args); builder.Services.AddHostedService<Worker>(); // Add health checks builder.Services.AddHealthChecks() .AddCheck<WorkerHealthCheck>("worker_health") .AddResourceUtilizationHealthCheck(); // Add HTTP endpoint for health checks builder.Services.AddHealthChecksUI(); // Or use simple TCP listener for Kubernetes builder.Services.AddSingleton<TcpHealthProbeService>(); builder.Services.AddHostedService(sp => sp.GetRequiredService<TcpHealthProbeService>()); var host = builder.Build(); host.Run(); ``` ### Custom Health Check ```csharp public class WorkerHealthCheck : IHealthCheck { private readonly WorkerState _workerState; public WorkerHealthCheck(WorkerState workerState) { _workerState = workerState; } public Task<HealthCheckResult> CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default) { if (_workerState.LastSuccessfulRun > DateTime.UtcNow.AddMinutes(-5)) { return Task.FromResult(HealthCheckResult.Healthy( $"Last successful run: {_workerState.LastSuccessfulRun}")); } return Task.FromResult(HealthCheckResult.Unhealthy( $"No successful run since: {_workerState.LastSuccessfulRun}")); } } // Shared state public class WorkerState { public DateTime LastSuccessfulRun { get; set; } = DateTime.UtcNow; public bool IsProcessing { get; set; } } ``` ## Graceful Shutdown Pattern ### Proper Shutdown Handling ```csharp public class GracefulWorker : BackgroundService { private readonly ILogger<GracefulWorker> _logger; private int _currentWorkItemId; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("Worker starting"); while (!stoppingToken.IsCancellationRequested) { _currentWorkItemId = GetNextWorkItemId(); try { // Pass cancellation token to all async operations await ProcessWorkItemAsync(_currentWorkItemId, stoppingToken); } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { _logger.LogInformation( "Shutdown requested, stopping after work item {Id}", _currentWorkItemId); break; } } } public override async Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("Worker stopping gracefully"); await base.StopAsync(cancellationToken); _logger.LogInformation("Worker stopped"); } } ``` ## Windows Service Deployment ### Configuring as Windows Service ```csharp // Program.cs var builder = Host.CreateApplicationBuilder(args); builder.Services.AddWindowsService(options => { options.ServiceName = "My Worker Service"; }); builder.Services.AddHostedService<Worker>(); var host = builder.Build(); host.Run(); ``` ### Project File Settings ```xml <Project Sdk="Microsoft.NET.Sdk.Worker"> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> <RuntimeIdentifier>win-x64</RuntimeIdentifier> <PublishSingleFile>true</PublishSingleFile> <SelfContained>true</SelfContained> </PropertyGroup> </Project> ``` ## Best Practices 1. **Use BackgroundService as base class** - Handles `StartAsync`/`StopAsync` boilerplate and cancellation management 2. **Create scopes for scoped dependencies** - Use `IServiceScopeFactory` to resolve scoped services like DbContext 3. **Propagate cancellation tokens everywhere** - Pass to all async methods for responsive shutdown 4. **Wrap work in try-catch** - Unhandled exceptions stop the service completely 5. **Use PeriodicTimer for timed tasks** - Cleaner than `Task.Delay` with proper cancellation support 6. **Add health checks** - Essential for Kubernetes liveness/readiness probes 7. **Avoid blocking StartAsync** - Long initialization delays other hosted services 8. **Call base methods when overriding** - Always call `await base.StartAsync()` and `await base.StopAsync()` 9. **Publish as single file for Windows Service** - Reduces deployment complexity and errors 10. **Consider scaling requirements** - Separate worker projects if independent scaling is needed ## Anti-Patterns to Avoid | Anti-Pattern | Why It's Bad | Better Approach | |--------------|--------------|-----------------| | Ad-hoc `while(true)` loops | No graceful shutdown, poor lifecycle | Use `BackgroundService` | | Ignoring cancellation token | Ungraceful shutdown, resource leaks | Propagate token to all async calls | | Injecting scoped services directly | Captive dependencies, memory leaks | Use `IServiceScopeFactory` | | Unhandled exceptions in `ExecuteAsync` | Silently stops the worker | Wrap in try-catch, log, continue | | Long-running `StartAsync` | Blocks other services from starting | Move work to `ExecuteAsync` | | `async void` methods | Crashes process on exception | Use `async Task` | | Missing health checks | No visibility into worker status | Implement `IHealthCheck` | | Polling with tight loops | CPU waste, no responsiveness | Use `PeriodicTimer` or event-driven | | Not overriding `StopAsync` | Missed cleanup opportunity | Override for graceful cleanup | | Singleton DbContext | Not thread-safe, stale data | Create scopes per operation | ## Deliver - well-behaved worker processes and hosted services - predictable startup and shutdown behavior - proper scoped dependency handling - health checks for production observability - retry and poison-message handling for queue work ## Validate - cancellation token propagated and shutdown honored - scoped services resolved within proper scopes - exception handling prevents service crashes - health checks report accurate worker status - runtime behavior visible through logs or telemetry - no blocking calls in async context
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.