.claude/skills/background-hosted-service/SKILL.md
Use when implementing background work, queued processing, or recurring tasks, or when replacing fire-and-forget Task.Run calls that lose exceptions silently. Covers BackgroundService with PeriodicTimer, Channel<T> for high-performance producer-consumer pipelines, IServiceScopeFactory for per-iteration DI scopes, and graceful shutdown with CancellationToken propagation. Domain: Infrastructure, Concurrency, Worker Patterns. Level: Intermediate. Tags: background-service, hosted-service, channels, worker, periodic-timer.
npx skillsauth add klod68/littlerae background-hosted-serviceInstall 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.
Without a structured approach to background work:
Task.Run calls lose exceptions silentlyConcurrentQueue + polling loopsBackgroundService + System.Threading.ChannelsUse the IHostedService / BackgroundService infrastructure for managed background work, and Channel<T> for high-performance producer-consumer patterns.
public sealed class CleanupService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<CleanupService> _logger;
private readonly TimeSpan _interval = TimeSpan.FromMinutes(30);
public CleanupService(IServiceProvider serviceProvider, ILogger<CleanupService> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(_interval);
while (await timer.WaitForNextTickAsync(stoppingToken))
{
try
{
await using var scope = _serviceProvider.CreateAsyncScope();
var repository = scope.ServiceProvider.GetRequiredService<IOrderRepository>();
var cleaned = await repository.CleanupExpiredOrdersAsync(stoppingToken);
_logger.LogInformation("Cleaned {Count} expired orders", cleaned);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
_logger.LogError(ex, "Cleanup failed — will retry at next interval");
}
}
}
}
// 1. Define the channel as a singleton service
public sealed class EmailQueue
{
private readonly Channel<EmailMessage> _channel =
Channel.CreateBounded<EmailMessage>(new BoundedChannelOptions(1000)
{
FullMode = BoundedChannelFullMode.Wait,
SingleReader = true,
SingleWriter = false
});
public ChannelWriter<EmailMessage> Writer => _channel.Writer;
public ChannelReader<EmailMessage> Reader => _channel.Reader;
}
// 2. Producer (in request handler)
public class OrderService
{
private readonly EmailQueue _emailQueue;
public async Task PlaceOrderAsync(Order order, CancellationToken ct)
{
// ... save order
await _emailQueue.Writer.WriteAsync(
new EmailMessage(order.CustomerEmail, "Order Confirmed", $"Order #{order.Id}"),
ct);
}
}
// 3. Consumer (background service)
public sealed class EmailSenderService : BackgroundService
{
private readonly EmailQueue _queue;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<EmailSenderService> _logger;
public EmailSenderService(EmailQueue queue, IServiceProvider serviceProvider,
ILogger<EmailSenderService> logger)
{
_queue = queue;
_serviceProvider = serviceProvider;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await foreach (var message in _queue.Reader.ReadAllAsync(stoppingToken))
{
try
{
await using var scope = _serviceProvider.CreateAsyncScope();
var sender = scope.ServiceProvider.GetRequiredService<IEmailSender>();
await sender.SendAsync(message, stoppingToken);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
_logger.LogError(ex, "Failed to send email to {Recipient}", message.To);
// Optionally: re-enqueue with retry count
}
}
}
}
public sealed class OrderEventListener : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<OrderEventListener> _logger;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await using var scope = _serviceProvider.CreateAsyncScope();
var bus = scope.ServiceProvider.GetRequiredService<IMessageBus>();
await bus.SubscribeAsync<OrderPlaced>(async (message, ct) =>
{
await using var innerScope = _serviceProvider.CreateAsyncScope();
var handler = innerScope.ServiceProvider.GetRequiredService<IOrderPlacedHandler>();
await handler.HandleAsync(message, ct);
}, stoppingToken);
}
}
// Periodic timer
builder.Services.AddHostedService<CleanupService>();
// Channel-based queue
builder.Services.AddSingleton<EmailQueue>();
builder.Services.AddHostedService<EmailSenderService>();
// Options-driven interval
builder.Services.Configure<CleanupOptions>(builder.Configuration.GetSection("Cleanup"));
Task.Run without error handling.await.ExecuteAsync terminates the entire application in .NET 6+. ALWAYS wrap the loop body in try/catch and log errors. Never let exceptions escape.Thread.Sleep, .Result) starves the thread pool. Always use await with PeriodicTimer, Channel.ReadAllAsync, or Task.Delay.BackgroundService is a singleton. Never inject scoped services (DbContext, repositories) via constructor. Create a scope per iteration: _serviceProvider.CreateAsyncScope().stoppingToken is cancelled when the host shuts down. Respect it in loops. PeriodicTimer.WaitForNextTickAsync and Channel.ReadAllAsync handle this automatically.BoundedChannelOptions with a reasonable capacity. Unbounded channels can eat all memory if the consumer is slower than the producer.SingleReader = false if you need parallel consumers, but then ensure your processing logic is thread-safe.tools
Use when cross-cutting concerns (logging, metrics, validation, authorization) are tangled into command handlers or service methods, when building database command pipelines with reorderable concerns, or when HTTP client pipelines or message handlers need composable, independently-replaceable processing stages. Covers ICommandInterceptor interface, InterceptorPipeline with reverse-chain construction, zero-cost Empty sentinel to skip overhead when no interceptors are registered, and ConfigureAwait(false) discipline for library code. Domain: Architecture, Cross-Cutting Concerns. Level: Intermediate. Tags: interceptor, pipeline, middleware, decorator, cross-cutting-concerns.
development
Use when writing integration tests for Razor Pages, MVC, or Minimal API applications to validate routing, middleware, page rendering, and HTTP behavior without a browser or live server, or when adding fast smoke tests to a CI pipeline. Covers WebApplicationFactory<Program> setup with public partial class Program, in-memory test server, AngleSharp HTML parsing, CSS selector assertions, redirect and status code testing, and a shared static fixture pattern for minimal per-test startup overhead. Domain: Testing, ASP.NET Core. Level: Intermediate. Tags: integration-testing, webapplicationfactory, razor-pages, anglesharp, http-testing.
development
Use when designing indexes for new tables, diagnosing slow queries that are not using indexes efficiently, reviewing index fragmentation and maintenance, or when the current indexing strategy results in key lookups, table scans, or missing index warnings. Covers clustered index key selection (narrow, unique, ever-increasing), non-clustered index design for query patterns, covering indexes with INCLUDE columns, filtered indexes for subset queries, composite index column ordering, DMV-based monitoring for missing and unused indexes, and rebuild vs reorganize maintenance thresholds. Domain: Database, Performance. Level: Intermediate. Tags: index, sql-server, covering-index, filtered-index, performance, dmv, maintenance.
development
Use when building a searchable in-memory catalog or registry for documentation sites, admin panels, or type/API browsers where you need keyword matching, fuzzy search, and ranked results without an external search engine or database. Covers RegistryService with weighted scoring across name, description, keywords, and method names; Levenshtein fuzzy matching; synonym expansion; category and subcategory filtering; and singleton DI registration for datasets of hundreds to low thousands of items. Domain: Search, Data Access Patterns. Level: Intermediate. Tags: search, registry, fuzzy-matching, in-memory, catalog, filtering.