Install
$ agentstack add skill-managedcode-dotnet-skills-worker-services ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
.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](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
- Use BackgroundService as your base class:
- Provides standard
StartAsync/StopAsynchandling - Focus on implementing
ExecuteAsynconly - Proper cancellation token management built-in
- 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
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 _logger;
public Worker(ILogger 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 _logger;
private readonly IServiceScopeFactory _scopeFactory;
private readonly TimeSpan _period = TimeSpan.FromMinutes(1);
public TimedWorker(ILogger 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();
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 _logger;
public ScopedWorker(IServiceScopeFactory scopeFactory, ILogger 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();
var service = scope.ServiceProvider.GetRequiredService();
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 _logger;
private readonly IServiceScopeFactory _scopeFactory;
private readonly IBackgroundTaskQueue _taskQueue;
public QueueWorker(
ILogger 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 workItem);
ValueTask> DequeueAsync(
CancellationToken cancellationToken);
}
Health Checks for Workers
Adding Health Check Endpoint
// Program.cs
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService();
// Add health checks
builder.Services.AddHealthChecks()
.AddCheck("worker_health")
.AddResourceUtilizationHealthCheck();
// Add HTTP endpoint for health checks
builder.Services.AddHealthChecksUI();
// Or use simple TCP listener for Kubernetes
builder.Services.AddSingleton();
builder.Services.AddHostedService(sp => sp.GetRequiredService());
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 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 _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();
var host = builder.Build();
host.Run();
Project File Settings
net8.0
win-x64
true
true
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
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: managedcode
- Source: managedcode/dotnet-skills
- License: MIT
- Homepage: https://skills.managed-code.com
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.