【ASP.NET CORE】 9. 托管服务

本系列专栏基于杨中科老师的《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注册该服务,应用启动后即可自动执行定时导出任务。


总结

  1. 托管服务基于 BackgroundService 实现标准化后台任务开发,是实现定时任务、数据同步、应用预热等后台逻辑的官方方案。
  2. 异常必须捕获 :默认未处理异常会导致程序退出,生产环境必须用 try-catch 包裹业务逻辑并记录日志;
  3. DI 规范使用 :托管服务为单例生命周期,禁止直接注入 Scoped/Transient 服务,必须通过 IServiceScopeFactory 创建作用域解析短生命周期对象。
相关推荐
百万蹄蹄向前冲3 小时前
支付宝 VS 微信 小程序差异
前端·后端·微信小程序
Francek Chen3 小时前
【大数据存储与管理】分布式数据库HBase:03 HBase数据模型
大数据·数据库·hadoop·分布式·hdfs·hbase
做cv的小昊5 小时前
大语言模型系统:【CMU 11-868】课程学习笔记02——GPU编程基础1(GPU Programming Basics 1)
人工智能·笔记·学习·语言模型·llm·transformer·agent
小吴编程之路10 小时前
MySQL 索引核心特性深度解析:从底层原理到实操应用
数据库·mysql
~莫子10 小时前
MySQL集群技术
数据库·mysql
凤山老林10 小时前
SpringBoot 使用 H2 文本数据库构建轻量级应用
java·数据库·spring boot·后端
就不掉头发10 小时前
Linux与数据库进阶
数据库
与衫10 小时前
Gudu SQL Omni 技术深度解析
数据库·sql
清汤饺子10 小时前
用 Cursor 半年了,效率还是没提升?是因为你没用对这 7 个功能
前端·后端·cursor