Skip to main content

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>();

BackgroundService 基类

推荐使用 BackgroundService

BackgroundServiceIHostedService 的抽象基类,提供了更简单的实现方式

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();
}

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 提供更强大的调度功能
  • 使用队列处理异步消息
  • 避免在启动时阻塞应用

相关资源