From dotnet-blazor
Implements .NET background tasks and worker services using BackgroundService, IHostedService, and Channel queues. For processing pending orders, queue workers, and scheduled jobs.
How this skill is triggered — by the user, by Claude, or both
Slash command
/dotnet-blazor:worker-servicesThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
```csharp
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>();
// 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();
});
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();
}
}
// 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();
IServiceScopeFactory to create scopes in workers (hosted services are singletons, scoped services like DbContext need their own scope)PeriodicTimer instead of Task.Delay for scheduled work (more accurate, respects cancellation)Channel<T> for producer/consumer queues (thread-safe, backpressure support)stoppingToken in all loops and pass it to async operationsnpx claudepluginhub markus41/claude --plugin dotnet-blazorHosted services, background jobs, outbox patterns, and graceful shutdown handling for ASP.NET Core applications. Includes patterns for reliable job processing, distributed systems, and lifecycle management. Use when implementing background processing in ASP.NET Core applications, handling outbox patterns for reliable message delivery, or managing graceful service shutdown.
Generates scheduled background jobs with Quartz.NET: job definitions, cron triggers, dependency injection, and persistent job stores.
Configures Render background workers for queue-based async job processing using Celery, Sidekiq, BullMQ, Asynq, Oban. Covers graceful shutdown with SIGTERM, Redis Key Value as broker with noeviction policy, and workers vs cron jobs.