plugins/dotnet-blazor/skills/worker-services/SKILL.md
Background tasks and worker services with IHostedService, BackgroundService, and .NET hosted service patterns
npx skillsauth add markus41/claude worker-servicesInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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 operationsdevelopment
Enhanced plan-authoring skill with Pre-Writing context gathering, task metadata, non-TDD templates, Red Flags, telemetry, and an automated plan linter. Use when you have a spec or requirements for a multi-step task, before touching code.
tools
Documentation intelligence engine with graph-based API docs, algorithm library, and drift detection
tools
Ultraplan cloud planning — kick off a plan in the cloud from your terminal, review and revise in the browser, then execute remotely or send back to CLI
tools
--- name: mcp description: Configure MCP servers for Claude Code — stdio vs HTTP, authentication, Tools/Resources/Prompts distinction, channels (CI webhook, mobile relay, Discord bridge, fakechat), and cost of always-loaded tools. Use this skill whenever adding an MCP server, debugging connection issues, choosing between MCP Tools vs Prompts vs Resources, installing channel servers, or managing .mcp.json. Triggers on: "MCP server", "mcp config", "add Obsidian MCP", "install context7", "channels"