
Worker Services
- 19 installs
- 466 repo stars
- Updated July 25, 2026
- managedcode/dotnet-skills
Helps with ai & agent building tasks.
About
worker-services is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- worker-services
- AI & Agent Building
- AI-coding skill
Worker Services by the numbers
- 19 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #10,571 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/managedcode/dotnet-skills --skill worker-servicesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 19 |
|---|---|
| repo stars | ★ 466 |
| Last updated | July 25, 2026 |
| Repository | managedcode/dotnet-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
.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
1. Use BackgroundService as your base class:
- Provides standard
StartAsync/StopAsynchandling - Focus on implementing
ExecuteAsynconly - 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
PeriodicTimerfor scheduled work
5. Add observability:
- Use health checks for readiness/liveness
- Expose metrics and structured logging
- Consider distributed locks for multi-instance
Current Upstream Notes
.NET runtimev9.0.17is servicing. For workers, rerun cancellation, graceful shutdown, WebSocket/HTTP client, and long-running loop checks after upgrading packages rather than changing architecture by default.- Use the refreshed Worker Services and hosted-service Learn pages for exact current hosting and health-check APIs when adding new worker entry points.
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
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
{
"version": "1.0.1",
"category": "Core",
"packages": [
"Microsoft.Extensions.Hosting"
]
}
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.
// 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:
// 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.
// 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:
// 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.
// 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:
// 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.
// 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:
// 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.
// 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:
// 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.
// BAD: Relying on default timeout
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<LongRunningWorker>();
// Default ShutdownTimeout is 30 secondsCorrection: Configure appropriate timeout:
// 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.
// 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:
// 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.
// BAD: Singleton DbContext registration
builder.Services.AddSingleton<AppDbContext>(); // Thread-unsafe, stale tracking
builder.Services.AddHostedService<Worker>();Correction: Register as scoped and use scope factory:
// GOOD: Scoped DbContext
builder.Services.AddDbContext<AppDbContext>();
builder.Services.AddHostedService<Worker>();
// In worker, create scopes for each unit of work9. Service Locator in Constructor
Problem: Resolving services in constructor can fail if service isn't registered.
// 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:
// 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.
// 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:
// 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.
// 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:
// 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.
// 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:
// 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.
// 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:
// 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.
// 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:
// 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.
// BAD: No health visibility
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();
var host = builder.Build();
host.Run();Correction: Add health checks:
// 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.
// 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:
// 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.
// 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:
// 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.
// 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:
// 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.
// 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:
// 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.
// 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:
// 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);
}
}
}
}
}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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
// 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:
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:
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):
// 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
// 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:
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:
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:
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 }