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.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download postpartum-genushyacinthus29-dotnet-skills-skills_dotnet-worker-services-bfa4ebd.zip · 12 KB
Part of postpartum-genushyacinthus29/dotnet-skills — 80 skills

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

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/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

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

No comments yet.

Reviews (0)

No reviews yet.

Related