AgentStack
SKILL verified MIT Self-run

Worker Services

skill-managedcode-dotnet-skills-worker-services · by managedcode

Build long-running .NET background services with `BackgroundService`, Generic Host, graceful shutdown, configuration, logging, and deployment patterns suited to workers and daemons. USE FOR: background services; scheduled workers; hosted services; worker extraction; graceful shutdown, health checks, and service hosting review. DO NOT USE FOR: unrelated stacks; generic tasks that do not need this…

No reviews yet
0 installs
4 views
0.0% view→install

Install

$ agentstack add skill-managedcode-dotnet-skills-worker-services

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No 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.

Are you the author of Worker Services? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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

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
  1. Handle scoped dependencies correctly:
  • Create service scopes for scoped services
  • No scope is created by default in hosted services
  1. Implement graceful shutdown:
  • Propagate cancellation tokens throughout
  • Complete work promptly when token fires
  • Avoid ungraceful shutdown at timeout
  1. Keep execution loop thin:
  • Move business logic to testable services
  • Handle exceptions to prevent service crashes
  • Use PeriodicTimer for scheduled work
  1. Add observability:
  • Use health checks for readiness/liveness
  • Expose metrics and structured logging
  • Consider distributed locks for multi-instance

Current Upstream Notes

  • .NET runtime v9.0.17 is 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

  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

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.