Use when creating, reviewing, or troubleshooting BackgroundService, IHostedService, Hangfire jobs, queue processing with Channel, timer services, or graceful shutdown in ASP.NET Core.
Use this skill when an ASP.NET Core application needs background processing — long-running tasks, queue consumers, scheduled work, or hosted lifecycle services.
BackgroundService or IHostedService.BackgroundService for simple in-process work loops that do not need persistence or dashboard visibility.BackgroundService over raw IHostedService for work that runs in a loop.ExecuteAsync and respect the CancellationToken for graceful shutdown.ExecuteAsync without handling — an unhandled exception will stop the service (and in .NET 6+ may terminate the host).Representative direction:
public class OrderProcessingWorker : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<OrderProcessingWorker> _logger;
public OrderProcessingWorker(
IServiceScopeFactory scopeFactory,
ILogger<OrderProcessingWorker> logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await using var scope = _scopeFactory.CreateAsyncScope();
var processor = scope.ServiceProvider.GetRequiredService<IOrderProcessor>();
await processor.ProcessPendingOrdersAsync(stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing orders");
}
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
}
}
Channel<T> for in-process producer-consumer patterns.Representative direction:
// Registration
builder.Services.AddSingleton(Channel.CreateBounded<WorkItem>(new BoundedChannelOptions(100)
{
FullMode = BoundedChannelFullMode.Wait
}));
// Consumer (BackgroundService)
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await foreach (var item in _channel.Reader.ReadAllAsync(stoppingToken))
{
await ProcessItemAsync(item, stoppingToken);
}
}
PeriodicTimer (.NET 6+) for scheduled work instead of Task.Delay in a loop.Representative direction:
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));
while (await timer.WaitForNextTickAsync(stoppingToken))
{
await DoScheduledWorkAsync(stoppingToken);
}
}
IServiceScopeFactory to create a scope per unit of work.IHostedLifecycleService for services that need hooks at Starting, Started, Stopping, and Stopped phases.CancellationToken passed to ExecuteAsync or StopAsync.HostOptions.ShutdownTimeout if background work needs more than the default 30 seconds to finish.BackgroundServiceExceptionBehavior.Ignore only if the service handles its own errors.Prefer Hangfire over BackgroundService with PeriodicTimer when the workload needs:
Hangfire.Core, Hangfire.AspNetCore, and a storage package (e.g., Hangfire.SqlServer, Hangfire.PostgreSql, Hangfire.InMemory for dev).Representative direction:
builder.Services.AddHangfire(config => config
.SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseSqlServerStorage(builder.Configuration.GetConnectionString("Hangfire")));
builder.Services.AddHangfireServer();
app.MapHangfireDashboard("/hangfire", new DashboardOptions
{
Authorization = [new HangfireDashboardAuthFilter()]
});
BackgroundJob.Enqueue or IBackgroundJobClient.Enqueue for one-off async work triggered by an API request.BackgroundJob.Enqueue<IEmailService>(svc => svc.SendWelcomeEmailAsync(userId, CancellationToken.None));
RecurringJob.AddOrUpdate with a cron expression for periodic work.AddOrUpdate at startup so the schedule is always current.RecurringJob.AddOrUpdate<IReportService>(
"daily-sales-report",
svc => svc.GenerateDailySalesReportAsync(CancellationToken.None),
Cron.Daily(hour: 6, minute: 0));
When a recurring job iteration must not start while the previous one is still running, apply [DisableConcurrentExecution] combined with [AutomaticRetry(Attempts = 0)] so a skipped execution does not queue a retry.
public class ReportService : IReportService
{
[DisableConcurrentExecution(timeoutInSeconds: 0)]
[AutomaticRetry(Attempts = 0)]
public async Task GenerateDailySalesReportAsync(CancellationToken cancellationToken)
{
// Long-running work — safe from overlapping invocations.
await BuildReportAsync(cancellationToken);
}
}
timeoutInSeconds: 0 means the new invocation does not wait for the lock — if the previous run holds it, the new one is skipped immediately.AutomaticRetry(Attempts = 0) so a skipped execution is not retried later, which would defeat the purpose.BackgroundJob.Schedule for work that should run after a delay.BackgroundJob.Schedule<IOrderService>(
svc => svc.CancelUnpaidOrderAsync(orderId, CancellationToken.None),
TimeSpan.FromHours(1));
BackgroundJob.ContinueJobWith to chain jobs.var parentId = BackgroundJob.Enqueue<IImportService>(svc => svc.ImportDataAsync(CancellationToken.None));
BackgroundJob.ContinueJobWith<INotificationService>(parentId,
svc => svc.NotifyImportCompleteAsync(CancellationToken.None));
CancellationToken in job methods for cooperative cancellation during shutdown.HttpContext in job arguments; pass IDs and resolve data inside the job.IServerFilter, IElectStateFilter) for cross-cutting concerns like logging or tenant context.CancellationToken is respected for graceful shutdown.IServiceScopeFactory (for BackgroundService) or through Hangfire's built-in DI activation (for Hangfire jobs).[DisableConcurrentExecution] when overlap is not safe.[DisableConcurrentExecution] with [AutomaticRetry(Attempts = 0)].