worker-services
Worker Services and Background Tasks
BackgroundService (Preferred)
public sealed class OrderProcessingWorker(
IServiceScopeFactory scopeFactory,
ILogger<OrderProcessingWorker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogInformation("Order processing worker started");
while (!stoppingToken.IsCancellationRequested)
{
try
{
using var scope = scopeFactory.CreateScope();
var orderService = scope.ServiceProvider.GetRequiredService<IOrderService>();
var pendingOrders = await orderService.GetPendingOrdersAsync(stoppingToken);
foreach (var order in pendingOrders)
{
await orderService.ProcessOrderAsync(order.Id, stoppingToken);
logger.LogInformation("Processed order {OrderId}", order.Id);
}
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
logger.LogError(ex, "Error processing orders");
}
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
}
}
// Registration
builder.Services.AddHostedService<OrderProcessingWorker>();
Queue-Based Worker (Channel)
// Shared queue
public sealed class BackgroundTaskQueue
{
private readonly Channel<Func<IServiceScopeFactory, CancellationToken, ValueTask>> _queue;
public BackgroundTaskQueue(int capacity = 100)
{
_queue = Channel.CreateBounded<Func<IServiceScopeFactory, CancellationToken, ValueTask>>(
new BoundedChannelOptions(capacity) { FullMode = BoundedChannelFullMode.Wait });
}
public async ValueTask QueueAsync(
Func<IServiceScopeFactory, CancellationToken, ValueTask> workItem,
CancellationToken ct = default)
{
await _queue.Writer.WriteAsync(workItem, ct);
}
public async ValueTask<Func<IServiceScopeFactory, CancellationToken, ValueTask>> DequeueAsync(
CancellationToken ct) =>
await _queue.Reader.ReadAsync(ct);
}
// Worker that processes the queue
public sealed class QueuedBackgroundWorker(
BackgroundTaskQueue taskQueue,
IServiceScopeFactory scopeFactory,
ILogger<QueuedBackgroundWorker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var workItem = await taskQueue.DequeueAsync(stoppingToken);
try
{
await workItem(scopeFactory, stoppingToken);
}
catch (Exception ex)
{
logger.LogError(ex, "Error executing queued work item");
}
}
}
}
// Enqueue from API endpoint
app.MapPost("/api/reports/generate", async (
ReportRequest request, BackgroundTaskQueue queue) =>
{
await queue.QueueAsync(async (scopeFactory, ct) =>
{
using var scope = scopeFactory.CreateScope();
var reportService = scope.ServiceProvider.GetRequiredService<IReportService>();
await reportService.GenerateAsync(request, ct);
});
return TypedResults.Accepted();
});
Timed Background Service
public sealed class CleanupWorker(
IServiceScopeFactory scopeFactory,
ILogger<CleanupWorker> logger) : BackgroundService
{
private readonly PeriodicTimer _timer = new(TimeSpan.FromHours(1));
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (await _timer.WaitForNextTickAsync(stoppingToken))
{
try
{
using var scope = scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var cutoff = DateTime.UtcNow.AddDays(-30);
var deleted = await db.TempFiles
.Where(f => f.CreatedAt < cutoff)
.ExecuteDeleteAsync(stoppingToken);
logger.LogInformation("Cleaned up {Count} expired temp files", deleted);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
logger.LogError(ex, "Cleanup failed");
}
}
}
public override void Dispose()
{
_timer.Dispose();
base.Dispose();
}
}
Standalone Worker Service
// Program.cs for a standalone worker (not a web app)
var builder = Host.CreateApplicationBuilder(args);
builder.AddServiceDefaults(); // Aspire integration
builder.Services.AddHostedService<EventConsumerWorker>();
builder.Services.AddDbContext<AppDbContext>(opts =>
opts.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
var host = builder.Build();
host.Run();
Key Patterns
- Always use
IServiceScopeFactoryto create scopes in workers (hosted services are singletons, scoped services like DbContext need their own scope) - Use
PeriodicTimerinstead ofTask.Delayfor scheduled work (more accurate, respects cancellation) - Use
Channel<T>for producer/consumer queues (thread-safe, backpressure support) - Handle exceptions in the loop body, not outside (prevents worker from stopping on error)
- Check
stoppingTokenin all loops and pass it to async operations - Log lifecycle events (started, stopped, errors) for observability
Reference
More from lobbi-docs/claude
vision-multimodal
Vision and multimodal capabilities for Claude including image analysis, PDF processing, and document understanding. Activate for image input, base64 encoding, multiple images, and visual analysis.
243design-system
Apply and manage the AI-powered design system with 50+ curated styles
126complex-reasoning
Multi-step reasoning patterns and frameworks for systematic problem solving. Activate for Chain-of-Thought, Tree-of-Thought, hypothesis-driven debugging, and structured analytical approaches that leverage extended thinking.
105gcp
Google Cloud Platform services including GKE, Cloud Run, Cloud Storage, BigQuery, and Pub/Sub. Activate for GCP infrastructure, Google Cloud deployment, and GCP integration.
73kanban
Kanban methodology including boards, WIP limits, flow metrics, and continuous delivery. Activate for Kanban boards, workflow visualization, and lean project management.
63debugging
Debugging techniques for Python, JavaScript, and distributed systems. Activate for troubleshooting, error analysis, log investigation, and performance debugging. Includes extended thinking integration for complex debugging scenarios.
59