关于 background-services
面向 ASP.NET Core 应用后台工作的开发技能,介绍托管服务生命周期及队列、定时任务、可靠消息、限流和优雅关闭模式。
主要功能
- 01BackgroundService 与 IHostedService 指引
- 02生命周期与取消处理
- 03基于 Channel 的任务队列
- 04Cron 与周期任务模式
- 05Outbox 与优雅关闭模式
适用场景
- 01在请求流程之外处理队列工作
- 02运行定时或周期任务
- 03通过可靠持久化处理消息
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill background-services安装后在 Claude Code 中输入/background-services确认已加载。
详细介绍
ASP.NET Core 中的后台服务
原理
后台服务对于将工作从请求管道中卸载、处理队列和执行计划任务至关重要。实现不当的后台服务可能导致数据丢失、任务成为孤儿任务以及资源泄漏。这些模式可确保生产应用中的后台处理可靠、可观测,并能优雅降级。
BackgroundService 与 IHostedService
| 特性 | BackgroundService | IHostedService |
|---|---|---|
| 用途 | 长时间运行的循环或持续性工作 | 启动/关闭钩子 |
| 方法 | 重写 ExecuteAsync | 实现 StartAsync + StopAsync |
| 生命周期 | 运行至取消或主机关机 | StartAsync 在启动时运行,StopAsync 在关闭时运行 |
| 适用场景 | 轮询队列、处理流、定期任务 | 数据库迁移、缓存预热、资源清理 |
模式 1:BackgroundService 基本结构
使用 BackgroundService 基类,以便统一管理生命周期并支持取消。
public sealed class OrderProcessorWorker(
IServiceScopeFactory scopeFactory,
ILogger<OrderProcessorWorker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogInformation("Order processor started");
while (!stoppingToken.IsCancellationRequested)
{
try
{
using var scope = scopeFactory.CreateScope();
var processor = scope.ServiceProvider
.GetRequiredService<IOrderProcessor>();
var processed = await processor.ProcessPendingAsync(stoppingToken);
if (processed == 0)
{
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
logger.LogError(ex, "Error processing orders");
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
}
logger.LogInformation("Order processor stopped");
}
}
// Registration
builder.Services.AddHostedService<OrderProcessorWorker>();
BackgroundService 的关键规则
- 始终创建作用域 --
BackgroundService注册为单例。注入IServiceScopeFactory,不要直接注入作用域服务。 - 始终处理异常 -- 默认情况下,
ExecuteAsync中未处理的异常会导致主机停止。请用 try/catch 包裹循环主体。 - 始终遵守停止令牌 -- 检查
stoppingToken.IsCancellationRequested,并将令牌传递给所有异步调用。 - 在空闲/出错时退避 -- 避免浪费 CPU 的紧密轮询循环。使用带有停止令牌的
Task.Delay。
模式 2:使用 IHostedService 实现启动/关闭钩子
启动钩子(缓存预热、迁移)
public sealed class CacheWarmupService(
IServiceScopeFactory scopeFactory,
ILogger<CacheWarmupService> logger) : IHostedService
{
public async Task StartAsync(CancellationToken cancellationToken)
{
logger.LogInformation("Warming caches");
using var scope = scopeFactory.CreateScope();
var cache = scope.ServiceProvider.GetRequiredService<IProductCache>();
await cache.WarmAsync(cancellationToken);
logger.LogInformation("Cache warmup complete");
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
启动 + 关闭(资源生命周期)
public sealed class MessageBusService(
ILogger<MessageBusService> logger) : IHostedService
{
private IConnection? _connection;
public async Task StartAsync(CancellationToken cancellationToken)
{
logger.LogInformation("Connecting to message bus");
_connection = await CreateConnectionAsync(cancellationToken);
}
public async Task StopAsync(CancellationToken cancellationToken)
{
logger.LogInformation("Disconnecting from message bus");
if (_connection is not null)
{
await _connection.CloseAsync(cancellationToken);
_connection = null;
}
}
private static Task<IConnection> CreateConnectionAsync(CancellationToken ct) =>
throw new NotImplementedException();
}
模式 3:托管服务生命周期
启动顺序
- 按照注册顺序,依次调用每个已注册服务的
IHostedService.StartAsync StartAsync完成后调用BackgroundService.ExecuteAsync(它会并发运行,主机不会等待它完成)- 所有
StartAsync调用完成后,主机即可开始处理请求
重要提示: ExecuteAsync 在将控制权交还给调用方之前不得阻塞。ExecuteAsync 中第一次执行 await 时,控制权会返回给主机。
public sealed class MyWorker : BackgroundService
{
public override async Task StartAsync(CancellationToken cancellationToken)
{
await InitializeAsync(cancellationToken);
await base.StartAsync(cancellationToken);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await DoWorkAsync(stoppingToken);
}
}
}
关闭顺序
- 触发
IHostApplicationLifetime.ApplicationStopping - 主机按照注册顺序的逆序对每个托管服务调用
StopAsync - 对于
BackgroundService,停止令牌会被取消,然后StopAsync会等待ExecuteAsync完成 - 触发
IHostApplicationLifetime.ApplicationStopped
模式 4:Outbox 模式实现可靠消息传递
在异步处理之前,先将消息以事务方式存储在数据库中,确保消息不会丢失。
public class OutboxMessage
{
public Guid Id { get; set; }
public required string Type { get; set; }
public required string Payload { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset? ProcessedAt { get; set; }
public string? Error { get; set; }
public int RetryCount { get; set; }
}
public interface IOutboxRepository
{
Task AddAsync(OutboxMessage message, CancellationToken ct = default);
Task<IReadOnlyList<OutboxMessage>> GetPendingAsync(int batchSize, CancellationToken ct = default);
Task MarkProcessedAsync(Guid messageId, CancellationToken ct = default);
Task MarkFailedAsync(Guid messageId, string error, CancellationToken ct = default);
}
public class OrderService
{
private readonly ApplicationDbContext _db;
private readonly IOutboxRepository _outbox;
public async Task<Order> CreateOrderAsync(CreateOrderRequest request)
{
await using var transaction = await _db.Database.BeginTransactionAsync();
try
{
var order = new Order { };
_db.Orders.Add(order);
var message = new OutboxMessage
{
Id = Guid.NewGuid(),
Type = nameof(OrderCreatedEvent),
Payload = JsonSerializer.Serialize(new OrderCreatedEvent
{
OrderId = order.Id,
CustomerEmail = request.CustomerEmail
}),
CreatedAt = DateTimeOffset.UtcNow
};
await _outbox.AddAsync(message);
await _db.SaveChangesAsync();
await transaction.CommitAsync();
return order;
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
}
public class OutboxProcessor : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<OutboxProcessor> _logger;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await using var scope = _serviceProvider.CreateAsyncScope();
var outbox = scope.ServiceProvider.GetRequiredService<IOutboxRepository>();
var publisher = scope.ServiceProvider.GetRequiredService<IEventPublisher>();
var messages = await outbox.GetPendingAsync(batchSize: 10, stoppingToken);
foreach (var message in messages)
{
try
{
await publisher.PublishAsync(message.Type, message.Payload, stoppingToken);
await outbox.MarkProcessedAsync(message.Id, stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process outbox message {MessageId}", message.Id);
await outbox.MarkFailedAsync(message.Id, ex.Message, stoppingToken);
}
}
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in outbox processor");
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
}
}
}
模式 5:优雅处理关闭
实现 IHostedLifecycleService,以便对启动和关闭顺序进行细粒度控制。
public class GracefulWorker : BackgroundService
{
private readonly IHostApplicationLifetime _lifetime;
private readonly ILogger<GracefulWorker> _logger;
private readonly Channel<WorkItem> _workChannel;
public GracefulWorker(
IHostApplicationLifetime lifetime,
ILogger<GracefulWorker> logger)
{
_lifetime = lifetime;
_logger = logger;
_workChannel = Channel.CreateUnbounded<WorkItem>();
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_lifetime.ApplicationStopping.Register(() =>
{
_logger.LogInformation("Shutdown requested, draining work channel...");
});
await foreach (var workItem in _workChannel.Reader.ReadAllAsync(stoppingToken))
{
try
{
await ProcessWorkItemAsync(workItem, stoppingToken);
}
catch (OperationCanceledException)
{
_logger.LogWarning("Work item {WorkId} cancelled due to shutdown", workItem.Id);
throw;
}
}
}
private async Task ProcessWorkItemAsync(WorkItem item, CancellationToken ct)
{
using var activity = new Activity("ProcessWorkItem").Start();
_logger.LogInformation("Processing work item {WorkId}", item.Id);
await Task.Delay(item.Duration, ct);
_logger.LogInformation("Completed work item {WorkId}", item.Id);
}
}
public class LifecycleAwareService : IHostedLifecycleService
{
private readonly ILogger<LifecycleAwareService> _logger;
public LifecycleAwareService(ILogger<LifecycleAwareService> logger) => _logger = logger;
public Task StartingAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Service starting...");
return Task.CompletedTask;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Service start called");
return Task.CompletedTask;
}
public Task StartedAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Service started successfully");
return Task.CompletedTask;
}
public Task StoppingAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Service stopping - cleanup starting...");
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Service stop called");
return Task.CompletedTask;
}
public Task StoppedAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Service stopped - cleanup complete");
return Task.CompletedTask;
}
}
主机关闭超时
builder.Services.Configure<HostOptions>(options =>
{
options.ShutdownTimeout = TimeSpan.FromSeconds(60);
});
模式 6:集成 Channels
由 BackgroundService 消费的、基于 Channel 的后台任务队列:
public sealed class BackgroundTaskQueue
{
private readonly Channel<Func<IServiceProvider, CancellationToken, Task>> _queue
= Channel.CreateBounded<Func<IServiceProvider, CancellationToken, Task>>(
new BoundedChannelOptions(100) { FullMode = BoundedChannelFullMode.Wait });
public ChannelWriter<Func<IServiceProvider, CancellationToken, Task>> Writer => _queue.Writer;
public ChannelReader<Func<IServiceProvider, CancellationToken, Task>> Reader => _queue.Reader;
}
public sealed class QueueProcessorWorker(
BackgroundTaskQueue queue,
IServiceScopeFactory scopeFactory,
ILogger<QueueProcessorWorker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (await queue.Reader.WaitToReadAsync(stoppingToken))
{
while (queue.Reader.TryRead(out var workItem))
{
try
{
using var scope = scopeFactory.CreateScope();
await workItem(scope.ServiceProvider, stoppingToken);
}
catch (Exception ex)
{
logger.LogError(ex, "Error executing queued work item");
}
}
}
}
}
// Registration
builder.Services.AddSingleton<BackgroundTaskQueue>();
builder.Services.AddHostedService<QueueProcessorWorker>();
模式 7:使用 Cron 表达式安排计划任务
使用 NCrontab 实现可靠的 Cron 计划,无需外部依赖。
public class ScheduledJobService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<ScheduledJobService> _logger;
private readonly CrontabSchedule _schedule;
private DateTime _nextRun;
public ScheduledJobService(IServiceProvider serviceProvider, ILogger<ScheduledJobService> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
_schedule = CrontabSchedule.Parse("0 2 * * *");
_nextRun = _schedule.GetNextOccurrence(DateTime.Now);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var now = DateTime.Now;
if (now >= _nextRun)
{
try
{
await ExecuteJobAsync(stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Scheduled job failed");
}
_nextRun = _schedule.GetNextOccurrence(now);
}
var delay = _nextRun - DateTime.Now;
if (delay > TimeSpan.Zero)
{
await Task.Delay(delay, stoppingToken);
}
}
}
private async Task ExecuteJobAsync(CancellationToken ct)
{
await using var scope = _serviceProvider.CreateAsyncScope();
var service = scope.ServiceProvider.GetRequiredService<IDailyReportService>();
await service.GenerateDailyReportAsync(ct);
}
}
模式 8:使用 PeriodicTimer 执行周期性工作
使用 PeriodicTimer 替代 Task.Delay,以实现更准确的周期性执行:
public sealed class HealthCheckReporter(
IServiceScopeFactory scopeFactory,
ILogger<HealthCheckReporter> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
while (await timer.WaitForNextTickAsync(stoppingToken))
{
try
{
using var scope = scopeFactory.CreateScope();
var reporter = scope.ServiceProvider.GetRequiredService<IHealthReporter>();
await reporter.ReportAsync(stoppingToken);
}
catch (Exception ex)
{
logger.LogError(ex, "Health check report failed");
}
}
}
}
模式 9:使用队列处理并进行速率限制
public class RateLimitedProcessor : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<RateLimitedProcessor> _logger;
private readonly Channel<EmailRequest> _channel;
private readonly SemaphoreSlim _semaphore;
public RateLimitedProcessor(
IServiceProvider serviceProvider,
ILogger<RateLimitedProcessor> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
_channel = Channel.CreateUnbounded<EmailRequest>();
_semaphore = new SemaphoreSlim(5, 5);
}
public ChannelWriter<EmailRequest> Writer => _channel.Writer;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var processingTasks = new List<Task>();
await foreach (var request in _channel.Reader.ReadAllAsync(stoppingToken))
{
await _semaphore.WaitAsync(stoppingToken);
var task = ProcessWithReleaseAsync(request, stoppingToken);
processingTasks.Add(task);
if (processingTasks.Count > 100)
{
processingTasks.RemoveAll(t => t.IsCompleted);
}
}
await Task.WhenAll(processingTasks);
}
private async Task ProcessWithReleaseAsync(EmailRequest request, CancellationToken ct)
{
try
{
await ProcessEmailAsync(request, ct);
}
finally
{
_semaphore.Release();
}
}
private async Task ProcessEmailAsync(EmailRequest request, CancellationToken ct)
{
await using var scope = _serviceProvider.CreateAsyncScope();
var emailService = scope.ServiceProvider.GetRequiredService<IEmailService>();
try
{
await emailService.SendAsync(request, ct);
_logger.LogInformation("Email sent to {Recipient}", request.To);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send email to {Recipient}", request.To);
}
await Task.Delay(TimeSpan.FromMilliseconds(100), ct);
}
}
反模式
// BAD: No cancellation token handling
public class BadProcessor : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (true) // Never stops!
{
await DoWorkAsync();
}
}
}
// GOOD: Proper cancellation handling
public class GoodProcessor : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await DoWorkAsync(stoppingToken);
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
}
}
// BAD: Swallowing all exceptions
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await DoWorkAsync();
}
catch (Exception)
{
// Silently swallowed - service appears healthy but isn't working!
}
}
}
// GOOD: Log and continue with backoff
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await DoWorkAsync(stoppingToken);
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Work failed");
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
}
}
}
// BAD: Using scoped services without creating scope
public class BadService : BackgroundService
{
private readonly ApplicationDbContext _db; // Scoped service in singleton!
public BadService(ApplicationDbContext db) => _db = db;
protected override async Task ExecuteAsync(CancellationToken ct)
{
var data = await _db.Orders.ToListAsync(ct); // Will fail after first request!
}
}
// GOOD: Create scope for each unit of work
public class GoodService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
public GoodService(IServiceProvider sp) => _serviceProvider = sp;
protected override async Task ExecuteAsync(CancellationToken ct)
{
await using var scope = _serviceProvider.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var data = await db.Orders.ToListAsync(ct);
}
}
// BAD: Blocking async code
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
DoWork().Wait(); // Blocks thread!
}
return Task.CompletedTask;
}
// GOOD: Use async/await throughout
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await DoWorkAsync(stoppingToken);
}
}
Agent 易踩的坑
- 不要将作用域服务注入 BackgroundService 构造函数——它们是单例。始终使用
IServiceScopeFactory。 - 不要使用
Task.Run执行后台工作——使用BackgroundService,以妥善管理生命周期并实现平滑关闭。 - 不要吞掉
OperationCanceledException——让它继续传播,或重新检查停止令牌。 - 不要使用
Thread.Sleep——使用await Task.Delay(duration, stoppingToken)或PeriodicTimer。 - 不要忘记注册——必须使用
AddHostedService<T>();仅实现接口不会产生任何作用。
参考资料
评论
支持 Markdown