mcpField
B
background-services

background-services

github.com/wshaddix开发8685 次安装35 周前更新
代码仓库

关于 background-services

面向 ASP.NET Core 应用后台工作的开发技能,介绍托管服务生命周期及队列、定时任务、可靠消息、限流和优雅关闭模式。

主要功能

  1. 01BackgroundService 与 IHostedService 指引
  2. 02生命周期与取消处理
  3. 03基于 Channel 的任务队列
  4. 04Cron 与周期任务模式
  5. 05Outbox 与优雅关闭模式

适用场景

  1. 01在请求流程之外处理队列工作
  2. 02运行定时或周期任务
  3. 03通过可靠持久化处理消息

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/wshaddix/dotnet-skills --skill background-services

安装后在 Claude Code 中输入/background-services确认已加载。

详细介绍

ASP.NET Core 中的后台服务

原理

后台服务对于将工作从请求管道中卸载、处理队列和执行计划任务至关重要。实现不当的后台服务可能导致数据丢失、任务成为孤儿任务以及资源泄漏。这些模式可确保生产应用中的后台处理可靠、可观测,并能优雅降级。


BackgroundService 与 IHostedService

特性BackgroundServiceIHostedService
用途长时间运行的循环或持续性工作启动/关闭钩子
方法重写 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 的关键规则
  1. 始终创建作用域 -- BackgroundService 注册为单例。注入 IServiceScopeFactory,不要直接注入作用域服务。
  2. 始终处理异常 -- 默认情况下,ExecuteAsync 中未处理的异常会导致主机停止。请用 try/catch 包裹循环主体。
  3. 始终遵守停止令牌 -- 检查 stoppingToken.IsCancellationRequested,并将令牌传递给所有异步调用。
  4. 在空闲/出错时退避 -- 避免浪费 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:托管服务生命周期

启动顺序
  1. 按照注册顺序,依次调用每个已注册服务的 IHostedService.StartAsync
  2. StartAsync 完成后调用 BackgroundService.ExecuteAsync(它会并发运行,主机不会等待它完成)
  3. 所有 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);
        }
    }
}
关闭顺序
  1. 触发 IHostApplicationLifetime.ApplicationStopping
  2. 主机按照注册顺序的逆序对每个托管服务调用 StopAsync
  3. 对于 BackgroundService,停止令牌会被取消,然后 StopAsync 会等待 ExecuteAsync 完成
  4. 触发 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 易踩的坑

  1. 不要将作用域服务注入 BackgroundService 构造函数——它们是单例。始终使用 IServiceScopeFactory。
  2. 不要使用 Task.Run 执行后台工作——使用 BackgroundService,以妥善管理生命周期并实现平滑关闭。
  3. 不要吞掉 OperationCanceledException——让它继续传播,或重新检查停止令牌。
  4. 不要使用 Thread.Sleep——使用 await Task.Delay(duration, stoppingToken) 或 PeriodicTimer。
  5. 不要忘记注册——必须使用 AddHostedService<T>();仅实现接口不会产生任何作用。

参考资料

评论

支持 Markdown