ASP.NET Core 后台服务
什么是后台服务?
核心概念
后台服务(Background Services)是在应用程序主进程中运行的长期运行任务,用于:
- 定时任务执行
- 消息队列处理
- 数据同步
- 缓存更新
- 日志清理
- 健康检查
IHostedService 接口
基本实现
public interface IHostedService
{
Task StartAsync(CancellationToken cancellationToken);
Task StopAsync(CancellationToken cancellationToken);
}
- 基本实现
- 使用作用域服务
public class MyHostedService : IHostedService
{
private readonly ILogger<MyHostedService> _logger;
public MyHostedService(ILogger<MyHostedService> logger)
{
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("后台服务启动");
// 启动后台任务
Task.Run(() => DoWorkAsync(cancellationToken), cancellationToken);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("后台服务停止");
return Task.CompletedTask;
}
private async Task DoWorkAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
_logger.LogInformation("执行后台任务: {Time}", DateTime.Now);
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
}
}
}
// 注册服务
builder.Services.AddHostedService<MyHostedService>();
public class ScopedProcessingService : IHostedService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<ScopedProcessingService> _logger;
private Timer? _timer;
public ScopedProcessingService(
IServiceProvider serviceProvider,
ILogger<ScopedProcessingService> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("作用域服务启动");
// 每 10 秒执行一次
_timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(10));
return Task.CompletedTask;
}
private void DoWork(object? state)
{
using var scope = _serviceProvider.CreateScope();
// 从作用域获取服务
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var repository = scope.ServiceProvider.GetRequiredService<IUserRepository>();
try
{
// 执行数据库操作
var users = repository.GetAll();
_logger.LogInformation("处理 {Count} 个用户", users.Count());
}
catch (Exception ex)
{
_logger.LogError(ex, "后台任务执行失败");
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("作用域服务停止");
_timer?.Change(Timeout.Infinite, 0);
_timer?.Dispose();
return Task.CompletedTask;
}
}
BackgroundService 基类
推荐使用 BackgroundService
BackgroundService
是 IHostedService
的抽象基类,提供了更简单的实现方式
1. 基本用法
public class DataSyncService : BackgroundService
{
private readonly ILogger<DataSyncService> _logger;
private readonly IConfiguration _configuration;
public DataSyncService(
ILogger<DataSyncService> logger,
IConfiguration configuration)
{
_logger = logger;
_configuration = configuration;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("数据同步服务启动");
// 等待应用完全启动
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
while (!stoppingToken.IsCancellationRequested)
{
try
{
await SyncDataAsync(stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "数据同步失败");
}
// 每 30 分钟同步一次
await Task.Delay(TimeSpan.FromMinutes(30), stoppingToken);
}
_logger.LogInformation("数据同步服务停止");
}
private async Task SyncDataAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("开始同步数据: {Time}", DateTime.Now);
// 执行同步逻辑
await Task.Delay(1000, cancellationToken);
_logger.LogInformation("数据同步完成");
}
public override Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("数据同步服务正在停止...");
return base.StopAsync(cancellationToken);
}
}
2. 定时任务服务
public class ScheduledTaskService : BackgroundService
{
private readonly ILogger<ScheduledTaskService> _logger;
private readonly IServiceProvider _serviceProvider;
public ScheduledTaskService(
ILogger<ScheduledTaskService> logger,
IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("定时任务服务启动");
while (!stoppingToken.IsCancellationRequested)
{
var now = DateTime.Now;
// 计算距离下一个整点的时间
var nextRun = now.Date.AddHours(now.Hour + 1);
var delay = nextRun - now;
_logger.LogInformation("下次执行时间: {NextRun}", nextRun);
try
{
await Task.Delay(delay, stoppingToken);
if (!stoppingToken.IsCancellationRequested)
{
await ExecuteTasksAsync(stoppingToken);
}
}
catch (TaskCanceledException)
{
_logger.LogInformation("定时任务被取消");
}
catch (Exception ex)
{
_logger.LogError(ex, "定时任务执行失败");
}
}
}
private async Task ExecuteTasksAsync(CancellationToken cancellationToken)
{
using var scope = _serviceProvider.CreateScope();
var currentHour = DateTime.Now.Hour;
// 根据时间执行不同的任务
switch (currentHour)
{
case 1: // 凌晨 1 点:清理日志
await CleanupLogsAsync(scope, cancellationToken);
break;
case 6: // 早上 6 点:生成报表
await GenerateReportsAsync(scope, cancellationToken);
break;
case 12: // 中午 12 点:同步数据
await SyncDataAsync(scope, cancellationToken);
break;
case 18: // 下午 6 点:发送通知
await SendNotificationsAsync(scope, cancellationToken);
break;
default:
_logger.LogInformation("无计划任务: {Hour}:00", currentHour);
break;
}
}
private async Task CleanupLogsAsync(IServiceScope scope, CancellationToken cancellationToken)
{
_logger.LogInformation("开始清理日志...");
await Task.Delay(1000, cancellationToken);
_logger.LogInformation("日志清理完成");
}
private async Task GenerateReportsAsync(IServiceScope scope, CancellationToken cancellationToken)
{
_logger.LogInformation("开始生成报表...");
await Task.Delay(1000, cancellationToken);
_logger.LogInformation("报表生成完成");
}
private async Task SyncDataAsync(IServiceScope scope, CancellationToken cancellationToken)
{
_logger.LogInformation("开始同步数据...");
await Task.Delay(1000, cancellationToken);
_logger.LogInformation("数据同步完成");
}
private async Task SendNotificationsAsync(IServiceScope scope, CancellationToken cancellationToken)
{
_logger.LogInformation("开始发送通知...");
await Task.Delay(1000, cancellationToken);
_logger.LogInformation("通知发送完成");
}
}
消息队列处理
1. 内存队列实现
// 队列消息
public class QueuedMessage
{
public string Id { get; set; } = Guid.NewGuid().ToString();
public string Type { get; set; } = string.Empty;
public string Payload { get; set; } = string.Empty;
public DateTime EnqueuedAt { get; set; } = DateTime.UtcNow;
}
// 后台队列服务
public interface IBackgroundTaskQueue
{
void QueueMessage(QueuedMessage message);
Task<QueuedMessage?> DequeueAsync(CancellationToken cancellationToken);
}
public class BackgroundTaskQueue : IBackgroundTaskQueue
{
private readonly Channel<QueuedMessage> _queue;
public BackgroundTaskQueue(int capacity = 100)
{
var options = new BoundedChannelOptions(capacity)
{
FullMode = BoundedChannelFullMode.Wait
};
_queue = Channel.CreateBounded<QueuedMessage>(options);
}
public void QueueMessage(QueuedMessage message)
{
if (!_queue.Writer.TryWrite(message))
{
throw new InvalidOperationException("队列已满,无法添加消息");
}
}
public async Task<QueuedMessage?> DequeueAsync(CancellationToken cancellationToken)
{
var message = await _queue.Reader.ReadAsync(cancellationToken);
return message;
}
}
// 队列处理服务
public class QueuedHostedService : BackgroundService
{
private readonly ILogger<QueuedHostedService> _logger;
private readonly IBackgroundTaskQueue _taskQueue;
private readonly IServiceProvider _serviceProvider;
public QueuedHostedService(
ILogger<QueuedHostedService> logger,
IBackgroundTaskQueue taskQueue,
IServiceProvider serviceProvider)
{
_logger = logger;
_taskQueue = taskQueue;
_serviceProvider = serviceProvider;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("队列处理服务启动");
while (!stoppingToken.IsCancellationRequested)
{
try
{
var message = await _taskQueue.DequeueAsync(stoppingToken);
if (message != null)
{
await ProcessMessageAsync(message, stoppingToken);
}
}
catch (OperationCanceledException)
{
// 服务正在停止
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "处理队列消息失败");
}
}
_logger.LogInformation("队列处理服务停止");
}
private async Task ProcessMessageAsync(QueuedMessage message, CancellationToken cancellationToken)
{
_logger.LogInformation(
"处理消息: {Id}, 类型: {Type}",
message.Id,
message.Type);
using var scope = _serviceProvider.CreateScope();
switch (message.Type)
{
case "Email":
var emailService = scope.ServiceProvider.GetRequiredService<IEmailService>();
await emailService.SendEmailAsync(message.Payload);
break;
case "Notification":
var notificationService = scope.ServiceProvider.GetRequiredService<INotificationService>();
await notificationService.SendNotificationAsync(message.Payload);
break;
default:
_logger.LogWarning("未知的消息类型: {Type}", message.Type);
break;
}
}
}
// 注册服务
builder.Services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();
builder.Services.AddHostedService<QueuedHostedService>();
// 使用示例
public class MyController : ControllerBase
{
private readonly IBackgroundTaskQueue _taskQueue;
public MyController(IBackgroundTaskQueue taskQueue)
{
_taskQueue = taskQueue;
}
[HttpPost("send-email")]
public IActionResult SendEmail([FromBody] EmailRequest request)
{
var message = new QueuedMessage
{
Type = "Email",
Payload = JsonSerializer.Serialize(request)
};
_taskQueue.QueueMessage(message);
return Accepted(new { Message = "邮件已加入发送队列" });
}
}
使用 Hangfire 实现定时任务
1. 安装 Hangfire
dotnet add package Hangfire.AspNetCore
dotnet add package Hangfire.SqlServer
# 或者使用内存存储(仅开发环境)
dotnet add package Hangfire.MemoryStorage
2. 配置 Hangfire
var builder = WebApplication.CreateBuilder(args);
// 添加 Hangfire 服务
builder.Services.AddHangfire(config =>
{
config
.SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseSqlServerStorage(builder.Configuration.GetConnectionString("DefaultConnection"));
});
// 添加 Hangfire 服务器
builder.Services.AddHangfireServer();
var app = builder.Build();
// 使用 Hangfire 仪表板
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
Authorization = new[] { new MyAuthorizationFilter() }
});
app.Run();
3. 创建后台任务
- 即发即忘
- 延迟任务
- 定期任务
- 延续任务
// 立即执行一次的任务
public class JobService
{
public void SendWelcomeEmail(string email)
{
// 发送欢迎邮件
Console.WriteLine($"发送欢迎邮件到: {email}");
}
}
// 在控制器中使用
[HttpPost("register")]
public IActionResult Register([FromBody] RegisterRequest request)
{
// 保存用户...
// 排队发送欢迎邮件
BackgroundJob.Enqueue<JobService>(x => x.SendWelcomeEmail(request.Email));
return Ok();
}
// 延迟 1 小时后执行
BackgroundJob.Schedule<JobService>(
x => x.SendReminderEmail(userId),
TimeSpan.FromHours(1));
// 指定具体时间执行
BackgroundJob.Schedule<JobService>(
x => x.SendReportEmail(reportId),
DateTime.UtcNow.AddDays(1));
// 使用 Cron 表达式定义定期任务
public class RecurringJobsSetup
{
public static void Configure()
{
// 每天早上 9 点执行
RecurringJob.AddOrUpdate<JobService>(
"daily-report",
x => x.GenerateDailyReport(),
"0 9 * * *");
// 每小时执行
RecurringJob.AddOrUpdate<JobService>(
"hourly-sync",
x => x.SyncData(),
Cron.Hourly);
// 每周一早上 8 点执行
RecurringJob.AddOrUpdate<JobService>(
"weekly-cleanup",
x => x.CleanupOldData(),
"0 8 * * MON");
// 每 5 分钟执行
RecurringJob.AddOrUpdate<JobService>(
"frequent-check",
x => x.CheckHealth(),
"*/5 * * * *");
}
}
// 在 Program.cs 中调用
app.UseHangfireDashboard();
RecurringJobsSetup.Configure();
// 在前一个任务完成后执行
var jobId = BackgroundJob.Enqueue<JobService>(x => x.ProcessData());
BackgroundJob.ContinueJobWith<JobService>(
jobId,
x => x.SendCompletionEmail());
// 链式任务
var job1 = BackgroundJob.Enqueue<JobService>(x => x.DownloadFile());
var job2 = BackgroundJob.ContinueJobWith<JobService>(job1, x => x.ProcessFile());
var job3 = BackgroundJob.ContinueJobWith<JobService>(job2, x => x.UploadResult());
4. Cron 表达式参考
表达式 | 说明 |
---|---|
* * * * * | 每分钟 |
*/5 * * * * | 每 5 分钟 |
0 * * * * | 每小时 |
0 0 * * * | 每天午夜 |
0 9 * * * | 每天早上 9 点 |
0 9 * * MON | 每周一早上 9 点 |
0 0 1 * * | 每月 1 日午夜 |
0 0 1 1 * | 每年 1 月 1 日午夜 |
或使用 Hangfire 预定义:
Cron.Minutely()
- 每分钟Cron.Hourly()
- 每小时Cron.Daily()
- 每天午夜Cron.Weekly()
- 每周日午夜Cron.Monthly()
- 每月 1 日午夜Cron.Yearly()
- 每年 1 月 1 日午夜
使用 Quartz.NET
1. 安装 Quartz.NET
dotnet add package Quartz
dotnet add package Quartz.Extensions.Hosting
2. 配置 Quartz
builder.Services.AddQuartz(q =>
{
// 使用内存存储
q.UseMicrosoftDependencyInjectionJobFactory();
// 定义 Job
var jobKey = new JobKey("DataSyncJob");
q.AddJob<DataSyncJob>(opts => opts.WithIdentity(jobKey));
// 定义触发器
q.AddTrigger(opts => opts
.ForJob(jobKey)
.WithIdentity("DataSyncJob-trigger")
.WithCronSchedule("0 0 * * * ?")); // 每小时执行
});
// 添加 Quartz 托管服务
builder.Services.AddQuartzHostedService(options =>
{
options.WaitForJobsToComplete = true;
});
3. 创建 Job
public class DataSyncJob : IJob
{
private readonly ILogger<DataSyncJob> _logger;
private readonly IServiceProvider _serviceProvider;
public DataSyncJob(
ILogger<DataSyncJob> logger,
IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
}
public async Task Execute(IJobExecutionContext context)
{
_logger.LogInformation("DataSyncJob 开始执行");
using var scope = _serviceProvider.CreateScope();
var dataService = scope.ServiceProvider.GetRequiredService<IDataService>();
try
{
await dataService.SyncAsync();
_logger.LogInformation("DataSyncJob 执行成功");
}
catch (Exception ex)
{
_logger.LogError(ex, "DataSyncJob 执行失败");
throw new JobExecutionException(ex);
}
}
}
最佳实践
最佳实践清单
1. 使用作用域服务
// ✅ 好:创建作用域
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
using var scope = _serviceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// 使用 dbContext
}
}
// ❌ 差:直接注入作用域服务
public class MyService : BackgroundService
{
private readonly AppDbContext _dbContext; // DbContext 应该是作用域的
public MyService(AppDbContext dbContext)
{
_dbContext = dbContext; // 危险!
}
}
2. 正确处理取消
// ✅ 好:检查取消令牌
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await DoWorkAsync(stoppingToken);
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
catch (OperationCanceledException)
{
// 服务正在停止
break;
}
}
}
// ❌ 差:忽略取消令牌
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (true) // 永远不会停止
{
await DoWorkAsync();
await Task.Delay(5000); // 没有传递取消令牌
}
}
3. 异常处理
// ✅ 好:捕获异常,服务继续运行
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await DoWorkAsync(stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "后台任务执行失败");
// 服务继续运行
}
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
}
// ❌ 差:异常导致服务停止
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await DoWorkAsync(stoppingToken); // 异常会终止服务
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
}
4. 避免阻塞启动
// ✅ 好:立即返回,异步执行
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("服务启动");
_ = Task.Run(() => DoLongRunningWork(cancellationToken), cancellationToken);
return Task.CompletedTask; // 立即返回
}
// ❌ 差:阻塞应用启动
public async Task StartAsync(CancellationToken cancellationToken)
{
await Task.Delay(TimeSpan.FromMinutes(5)); // 延迟应用启动
await DoLongRunningWork(cancellationToken);
}
总结
关键要点
- 使用
BackgroundService
简化后台服务实现 - 使用作用域服务处理数据库操作
- 正确处理取消令牌和异常
- Hangfire 适合复杂的任务调度需求
- Quartz.NET 提供更强大的调度功能
- 使用队列处理异步消息
- 避免在启动时阻塞应用