本系列专栏基于杨中科老师的《ASP.NET Core技术内幕与项目 实战》,本人记录梳理的学习笔记,有部分的增补和省略。更全面系统的讲解,请看杨老师的视频课:【.NET教程,.Net Core视频教程,杨中科主讲】。
一、托管服务
托管服务是ASP.NET Core中实现后台任务的标准方案,无需依赖第三方组件,依托主机生命周期管理,适配Web应用、控制台应用等多种宿主环境,核心用于处理脱离HTTP请求的后台逻辑,是实现定时任务、数据预热、异步同步等场景的最优解。
1. 核心概念与基础使用
(1)适用场景
托管服务适用于所有需要后台独立运行的任务,典型场景如下:
- 应用启动预热:服务器启动时预加载热点数据至缓存、初始化全局配置、预热数据库连接池,提升接口响应速度。
- 定时任务:每日凌晨定时备份数据库、定时清理日志/过期数据、定时统计业务数据并生成报表。
- 周期性任务:每隔固定时长同步跨库数据、轮询第三方接口获取数据、监控系统状态。
- 常驻后台任务:消息队列消费、长连接维护、后台日志采集等。
(2)实现方式
托管服务需实现IHostedService 接口,该接口定义了StartAsync(启动任务)、StopAsync(停止任务)核心方法。日常开发中无需直接实现该接口,推荐继承抽象类BackgroundService,仅需重写ExecuteAsync方法编写核心业务逻辑,简化开发流程。
(3)示例
以"延迟读取文件+周期性输出"为例,演示基础托管服务编写与注册:
cs
// 1. 编写托管服务类,继承BackgroundService
public class DemoBgService : BackgroundService
{
private readonly ILogger<DemoBgService> _logger;
public DemoBgService(ILogger<DemoBgService> logger)
{
_logger = logger;
}
// 重写核心执行方法
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
// 模拟启动延迟:等待5秒后执行首次逻辑
await Task.Delay(5000, stoppingToken);
_logger.LogInformation("托管服务启动完成,开始执行后台任务");
// 模拟读取文件操作
string filePath = Path.Combine(Directory.GetCurrentDirectory(), "bgTaskLog.txt");
await File.AppendAllTextAsync(filePath, $"任务启动时间:{DateTime.Now:yyyy-MM-dd HH:mm:ss}\n", stoppingToken);
// 周期性执行:每隔5秒输出日志,直至应用停止
while (!stoppingToken.IsCancellationRequested)
{
await File.AppendAllTextAsync(filePath, $"任务执行时间:{DateTime.Now:yyyy-MM-dd HH:mm:ss}\n", stoppingToken);
_logger.LogInformation("后台任务执行中,当前时间:{Time}", DateTime.Now);
await Task.Delay(5000, stoppingToken);
}
}
catch (TaskCanceledException)
{
_logger.LogInformation("后台任务因应用停止被取消,正常退出");
}
}
}
// 2. Program.cs中注册托管服务
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHostedService<DemoBgService>();
var app = builder.Build();
app.Run();
注册完成后,应用启动时托管服务自动启动,应用关闭时自动释放资源,无需手动管理生命周期。
2. 托管服务异常处理
托管服务的异常处理直接影响应用稳定性,ASP.NET Core 6及后续版本对异常机制做了优化,需严格遵循规范处理,避免应用崩溃。
(1)默认异常行为
从.NET 6开始,托管服务中未捕获的未处理异常,会直接导致主机停止、应用退出。这是微软的默认设计,目的是避免后台任务异常后"静默失败",导致业务逻辑异常却无法感知。
(2)异常忽略配置(不推荐)
若特殊场景需忽略异常、保证应用不退出,可修改HostOptions配置,但严禁生产环境使用:
cs
// Program.cs 配置忽略后台服务异常
builder.Services.Configure<HostOptions>(options =>
{
// 忽略异常,后台服务崩溃不影响主机运行
options.BackgroundServiceExceptionBehavior = BackgroundServiceExceptionBehavior.Ignore;
});
(3)推荐异常处理方案
核心原则:所有业务逻辑必须包裹try-catch,异常需记录日志+告警,避免异常上浮。优化后的ExecuteAsync代码如下:
cs
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("后台托管服务启动");
// 循环执行常驻任务
while (!stoppingToken.IsCancellationRequested)
{
try
{
// 核心业务逻辑:数据同步、定时导出等
await DoBusinessLogicAsync(stoppingToken);
await Task.Delay(5000, stoppingToken);
}
catch (Exception ex)
{
// 捕获所有异常,记录详细日志(含堆栈信息)
_logger.LogError(ex, "后台任务执行异常,异常信息:{Msg}", ex.Message);
// 生产环境可扩展:发送邮件/短信告警、推送监控平台
// 避免单次异常导致任务终止,延迟后重试
await Task.Delay(10000, stoppingToken);
}
}
_logger.LogInformation("后台托管服务停止");
}
// 封装核心业务逻辑
private async Task DoBusinessLogicAsync(CancellationToken stoppingToken)
{
// 业务代码:数据库操作、文件读写、接口调用等
}
3. 托管服务依赖注入
托管服务默认以**单例(Singleton)**生命周期注册到DI容器,这是DI注入的核心限制,也是最易踩坑的点。
(1)核心限制
单例生命周期的服务,禁止直接注入作用域(Scoped)、瞬态(Transient)服务,尤其是EF Core DbContext(默认Scoped),直接注入会导致上下文线程安全问题、数据紊乱,程序直接抛出异常。
(2)注入方式
通过注入IServiceScopeFactory手动创建作用域,在作用域内解析短生命周期服务,使用完毕后释放作用域,保证线程安全与资源释放。
cs
public class DataSyncBgService : BackgroundService
{
private readonly ILogger<DataSyncBgService> _logger;
private readonly IServiceScopeFactory _scopeFactory;
// 注入IServiceScopeFactory,而非直接注入DbContext
public DataSyncBgService(ILogger<DataSyncBgService> logger, IServiceScopeFactory scopeFactory)
{
_logger = logger;
_scopeFactory = scopeFactory;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
// 1. 创建作用域
using var scope = _scopeFactory.CreateScope();
// 2. 作用域内解析Scoped服务(EF Core上下文)
var dbCtx = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// 3. 执行业务逻辑
var userCount = await dbCtx.Users.CountAsync(stoppingToken);
_logger.LogInformation("当前用户总数:{Count}", userCount);
await Task.Delay(5000, stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "数据同步任务异常");
await Task.Delay(10000, stoppingToken);
}
}
}
}
二、实战:定时数据汇总导出
实现"每隔5秒统计用户注册数据,导出至文本文件"的常驻后台任务,贴合实际业务场景。
cs
public class DataExportBgService : BackgroundService
{
private readonly ILogger<DataExportBgService> _logger;
private readonly IServiceScopeFactory _scopeFactory;
public DataExportBgService(ILogger<DataExportBgService> logger, IServiceScopeFactory scopeFactory)
{
_logger = logger;
_scopeFactory = scopeFactory;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("数据定时导出服务启动");
while (!stoppingToken.IsCancellationRequested)
{
try
{
using var scope = _scopeFactory.CreateScope();
var dbCtx = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// 按日期统计用户注册数量
var userStat = await dbCtx.Users
.GroupBy(u => u.CreationTime.Date)
.Select(e => new {
Date = e.Key.ToString("yyyy-MM-dd"),
Count = e.Count()
})
.ToListAsync(stoppingToken);
// 导出至文件
string exportPath = Path.Combine(Directory.GetCurrentDirectory(), "UserStatExport.txt");
var exportContent = $"导出时间:{DateTime.Now:yyyy-MM-dd HH:mm:ss}\n";
foreach (var item in userStat)
{
exportContent += $"日期:{item.Date},注册用户数:{item.Count}\n";
}
await File.WriteAllTextAsync(exportPath, exportContent, stoppingToken);
_logger.LogInformation("数据导出完成,路径:{Path}", exportPath);
// 每隔5秒执行一次
await Task.Delay(5000, stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "数据导出任务执行失败");
await Task.Delay(10000, stoppingToken);
}
}
}
}
最后在Program.cs注册该服务,应用启动后即可自动执行定时导出任务。
总结
- 托管服务基于 BackgroundService 实现标准化后台任务开发,是实现定时任务、数据同步、应用预热等后台逻辑的官方方案。
- 异常必须捕获 :默认未处理异常会导致程序退出,生产环境必须用
try-catch包裹业务逻辑并记录日志; - DI 规范使用 :托管服务为单例生命周期,禁止直接注入 Scoped/Transient 服务,必须通过
IServiceScopeFactory创建作用域解析短生命周期对象。