C# Web开发教程(十一)后台主动服务

托管服务(HostedService,也称为"后台服务")

  • 托管服务,这个翻译是不准确的,我觉得应该翻译成主动服务:服务器自己主动发起的服务(任务)[相对于客户端发起请求,服务端才响应]

    • 它是一种在应用启动后自动运行、无需外部触发的服务
  • 使用场景

- 代码运行在后台,比如服务器启动的时候在后台预先加载数据到缓存,每天凌晨3点把数据导出到备份数据库,或者每隔5秒钟在两张表之间同步一次数据。

- 定时任务
  • 代码实现流程
- 实现IHostedService接口(用起来麻烦),一般编写从BackgroundService继承的类(用起来简单).
- 注册服务: services.AddHostedService<DemoBgService>();
  • 注意事项
- 一旦托管服务的代码有错,整个项目就无法启动
	- 可以把HostOptions.BackgroundServiceExceptionBehavior设置为Ignore,程序会忽略异常,而不是停止程序.
	- 不过不推荐这么搞,因为“异常应该被妥善的处理,而不是被忽略”.
	- 要在ExecuteAsync方法中把代码用try....catch包裹起来,当发生异常的时候,记录日志中或发警报等.
	
- 而在.net6.0之前的版本中,托管代码就算有异常,项目也可以启动起来(旧的设计其实不好,所以新的版本修复了)
// HostedServiceDemo1.cs

namespace WebApplicationAboutJWTConfigRun
{
    public class HostedServiceDemo1 : BackgroundService
    {
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            Console.WriteLine("HostedService1启动");
            await Task.Delay(3000);
            string txt = await File.ReadAllTextAsync("d:/text.txt");
            Console.WriteLine("文件读取中...");
            await Task.Delay(10000);
            Console.WriteLine(txt);
            Console.WriteLine("HostedService1启动任务结束!");

        }
    }
}

// Program.cs
......
builder.Services.AddSwaggerGen(c =>
{
   ......
});
// 主动服务(注册)
builder.Services.AddHostedService<HostedServiceDemo1>();
  • 故意触发异常的效果
namespace WebApplicationAboutJWTConfigRun
{
    public class HostedServiceDemo1 : BackgroundService
    {
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {

            try
            {
                Console.WriteLine("HostedService1启动");
                await Task.Delay(3000);
                string txt = await File.ReadAllTextAsync("d:/text.ext"); // 故意写错
                Console.WriteLine("文件读取中...");
                await Task.Delay(10000);
                Console.WriteLine(txt);
                Console.WriteLine("HostedService1启动任务结束!");
            }
            catch(Exception Ex)
            {
                Console.WriteLine("启动代码异常" + Ex);
            }
        }
    }
}

- 测试效果: 项目正常跑起来了,日志记录异常
......
启动代码异常System.IO.FileNotFoundException: Could not find file 'd:\text.ext'.
File name: 'd:\text.ext'
......
  • 托管服务中使用DI(依赖注入(DI)限制)
- 托管服务是以单例的生命周期注册到依赖注入容器中的。因此不能注入生命周期为范围或者瞬态的服务(不能直接注入 Scoped 或 Transient 服务(如 DbContext))。
  比如注入FCore的上下文的话,程序就会抛出异常。
    
- 可以通过构造方法注入一个IServiceScopeFactory服务,它可以用来创建一个IServiceScope对象,这样我们
  就可以通过IServiceScope来创建短生命周期的服务了(记得在Dispose中释放IServiceScope)。


  • 异常实例演示
// HostedServiceDemo2.cs

namespace WebApplicationAboutJWTConfigRun
{
    public class HostedServiceDemo2
    {
        public int Add(int a,int b)
        {
            return a + b;
        }
            
    }
}

// Program.cs
......
// 主动服务
builder.Services.AddHostedService<HostedServiceDemo1>();
// 注入生命周期为范围或者瞬态的服务
builder.Services.AddScoped<HostedServiceDemo2>();

// HostedServiceDemo1.cs 测试
namespace WebApplicationAboutJWTConfigRun
{
    public class HostedServiceDemo1 : BackgroundService
    {
        private readonly HostedServiceDemo2 hostedServiceDemo2;
		
		// 依赖注入
        public HostedServiceDemo1(HostedServiceDemo2 hostedServiceDemo2)
        {
            this.hostedServiceDemo2 = hostedServiceDemo2;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {

            try
            {
                Console.WriteLine("HostedService1启动");
                // 执行逻辑,这里会触发异常
                Console.WriteLine("执行HostedService2服务"+hostedServiceDemo2.Add(1,1));
                ......
            }
            catch(Exception Ex)
            {
                Console.WriteLine("启动代码异常" + Ex);
            }
            

        }
    }
}



  • 现在,修复上面的异常
namespace WebApplicationAboutJWTConfigRun
{
    public class HostedServiceDemo1 : BackgroundService
    {
        //private readonly HostedServiceDemo2 hostedServiceDemo2;

        //public HostedServiceDemo1(HostedServiceDemo2 hostedServiceDemo2)
        //{
        //    this.hostedServiceDemo2 = hostedServiceDemo2;
        //}
		
		// 声明 IServiceScope
        private IServiceScope serviceScope;
		
		// 依赖注入
        public HostedServiceDemo1(IServiceScopeFactory serviceScopeFactory)
        {
            this.serviceScope = serviceScopeFactory.CreateScope();
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {

            try
            {
                var testService = serviceScope.ServiceProvider.GetRequiredService<HostedServiceDemo2>();
                Console.WriteLine("HostedService1启动");
                Console.WriteLine("执行HostedService2服务"+ testService.Add(1,1));
                await Task.Delay(3000);
                string txt = await File.ReadAllTextAsync("d:/text.txt");
                Console.WriteLine("文件读取中...");
                await Task.Delay(10000);
                Console.WriteLine(txt);
                Console.WriteLine("HostedService1启动任务结束!");
            }
            catch(Exception Ex)
            {
                Console.WriteLine("启动代码异常" + Ex);
            }
            

        }

        public override void Dispose()
        {
            this.serviceScope.Dispose();
            base.Dispose();
        }
    }
}

✅ 总结要点

要点 说明
用途 后台任务、定时任务、数据同步等
实现方式 继承 BackgroundService
注册方式 AddHostedService()
异常处理 必须用 try-catch 包裹
DI 限制 不能直接注入 Scoped/Transient 服务
解决方案 使用 IServiceScopeFactory 创建作用域

连接数据库示例: 使用托管服务(BackgroundService)实现的定时数据导出任务

- 安装工具包

<Project Sdk="Microsoft.NET.Sdk.Web">

......

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.0" />
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />

	<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="6.0.0" />
	<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.0" />
	<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.0" />
	<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.0">
		<PrivateAssets>all</PrivateAssets>
		<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
	</PackageReference>
	  
  </ItemGroup>

</Project>




// MyUser.cs

using Microsoft.AspNetCore.Identity;

namespace WebApplicationAboutJWTConfigRun
{
    public class MyUser:IdentityUser<long>
    {
        public string? WeiXinAccount { get; set; }
    }
}

// MyRole.cs

using Microsoft.AspNetCore.Identity;

namespace WebApplicationAboutJWTConfigRun
{
    public class MyRole:IdentityRole<long>
    {
    }
}

// MyDbContext.cs

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace WebApplicationAboutJWTConfigRun
{
    public class MyDbContext : IdentityDbContext<MyUser,MyRole,long>
    {
        public MyDbContext(DbContextOptions<MyDbContext> options) : base(options)
        {
        }
    }
}

// Program.cs
......
builder.Services.AddDbContext<MyDbContext>(opt =>
{
    opt.UseSqlServer("Server=.;Database=idtest2;Trusted_Connection=True;");
});

- 作迁移和更新db

// ScheduledService.cs

using Microsoft.EntityFrameworkCore;

namespace WebApplicationAboutJWTConfigRun
{
    public class ScheduledService : BackgroundService
    {
    	// 解决单例服务无法直接使用Scoped服务的问题
        private readonly IServiceScope serviceScope;

        public ScheduledService(IServiceScopeFactory serviceScopeFactory)
        {
            // 创建独立的作用域来获取DbContext
            this.serviceScope = serviceScopeFactory.CreateScope();
        }


        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            try
            {
                var dbCtx = serviceScope.ServiceProvider.GetRequiredService<MyDbContext>();
                // 检查取消令牌:stoppingToken.IsCancellationRequested - 应用关闭时自动停止
                while (!stoppingToken.IsCancellationRequested)
                {
                    long c = await dbCtx.Users.LongCountAsync();
                    await File.WriteAllTextAsync("d:/text.txt",c.ToString());
                    await Task.Delay(5000);
                }
                
                Console.WriteLine("导出成功" + DateTime.Now);
            }
            catch (Exception ex)
            {
                Console.WriteLine($"出错了: {ex.Message}, 堆栈跟踪: {ex.StackTrace}");
            }
        }

        public override void Dispose()
        {
        	// 手动释放作用域:避免内存泄漏
            this.serviceScope.Dispose();
            // 调用基类Dispose:确保BackgroundService正确清理
            base.Dispose();
        }
    }
}

// Program.cs
......
// 主动服务
builder.Services.AddHostedService<ScheduledService>();
builder.Services.AddDbContext<MyDbContext>(opt =>
{
    ......  
}


posted @ 2025-11-03 11:55  清安宁  阅读(3)  评论(0)    收藏  举报