在AspNetCore中使用托管服务

在AspNetCore中使用托管服务

是托管服务主要用于什么场景?

托管服务主要用于那些不便于运行在前台,不便于写在控制器中的代码。比如服务器启动的时候在后台预先加载数据到缓存,
每天凌晨3点把数据导出到备份数据库,每隔5秒在两张表之间同步一些数据,定时清理过期数据等等。

如何使用托管服务?

托管服务有两种实现方式:

  1. 创建一个类实现IHostedService接口,这个方法完全自定义,需要注意生命周期管理,然后还要实现两个方法
    StartAsync(),StopAsync()最后在Program.cs中把服务注册到依赖注入容器。真心不建议使用这个

案例:使用IHostedService创建一个定时任务,每隔10秒执行一次,每次执行的任务是打印当前时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
namespace HostedService_AspNetCore01x02;
public class TestHostedService : IHostedService
{
private readonly ILogger<TestHostedService> _logger;
private Timer _timer;
private int _executionCount;
public TestHostedService(ILogger<TestHostedService> logger)
{
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("定时托管服务启动.");
_timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(10));
return Task.CompletedTask;
}
private void DoWork(object state)
{
int count = Interlocked.Increment(ref _executionCount);
_logger.LogInformation("定时托管服务运行 {Count}", count);
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("定时托管服务已停止.");
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
}

在Program.cs中注册服务

1
builder.Services.AddHostedService<TestHostedService>();

运行截图:
okjsnvaw

  1. 创建一个类,然后继承BackgroundService类,BackgroundService是官方提供的实现了IHostedService接口的抽象类。
    然后去重写ExecuteAsync()方法,这个方法用于定义任务逻辑。这个方法同样需要在Program.cs中把服务注册到依赖注入容器。
    使用BackgroundService的优势是它会自动管理生命周期,不需要自己去管理。推荐使用这个,问就是方便!!!

案例:使用BackgroundService创建一个定时任务,每隔10秒执行一次,每次执行的任务是打印当前时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
namespace HostedService_AspNetCore01x02;
public class TestHostedService : BackgroundService
{
private readonly ILogger<TestHostedService> _logger;
private int _executionCount;

public TestHostedService(ILogger<TestHostedService> logger)
{
_logger = logger;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("定时托管服务启动.");
await DoWork();
using PeriodicTimer timer = new(TimeSpan.FromSeconds(10));//可以在这里改时间间隔,秒,分,时,天
try
{
while (await timer.WaitForNextTickAsync(stoppingToken))
{
await DoWork();
}
}
catch (OperationCanceledException)
{
_logger.LogInformation("定时托管服务已停止.");
}
}

private async Task DoWork()
{
int count = Interlocked.Increment(ref _executionCount);
await Task.Delay(TimeSpan.FromSeconds(2));
_logger.LogInformation("定时托管服务运行 {Count}", count);
}
}

在Program.cs中注册服务

1
builder.Services.AddHostedService<TestHostedService>();

运行截图:
ajgdsherf
这就是一个简单的定时任务,每隔10秒执行一次,每次执行的任务是打印当前时间,注意这个托管服务我没有注入Scoped服务。

注意事项

除了这些,在使用托管服务的时候需要注意生命周期的冲突。

我们都知道托管服务是Singleton服务,而DbContext是Scoped服务。
所以直接把DbContext依赖注入到托管服务中会报错。
因为DbContext本来是在一个作用域(如一次 HTTP 请求、一个手动 CreateScope)内创建,作用域结束时自动释放;
但托管服务是单例周期会一直存在,导致托管服务会一直持有这一个DbContext而不会释放,一直到进程退出,
这就是连接泄漏、内存泄漏的根源。

如何在托管服务中使用DbContext?

同时使用 using 和 IServiceScopeFactory:

  1. 使用IServiceScopeFactory提供“临时作用域”对象,让 Scoped 的 DbContext 能被正确解析出来。
  2. 使用using确保作用域结束时自动 Dispose(归还连接、清缓存)。

关键处代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private readonly IServiceScopeFactory _serviceScopeFactory;
// 构造函数,把IServiceScopeFactory注入到托管服务中
public TestHostedService(IServiceScopeFactory serviceScopeFactory)
{
_serviceScopeFactory = serviceScopeFactory;
}
// 定时任务逻辑
private async Task DoWork()
{
// 使用IServiceScopeFactory来创建一个新的作用域,然后在作用域内注入DbContext
using var scope = _serviceScopeFactory.CreateScope();
var dateContext = scope.ServiceProvider.GetRequiredService<DateContext>();
// 查询数据库的逻辑
var toDoList = await dateContext.ToDoListTable
//注意:这里也是一个要点,在一对多关系的查询中非常重要:EF Core 默认不延迟加载;不加 Include 就得不到导航数据。
.Include(t => t.TransactionNodes)
.FirstOrDefaultAsync(x => x.Id == "1ca");
}

完整代码(这是一个一对多的事务列表及其事务节点的查询,这只是一个随便写的案例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
using Microsoft.EntityFrameworkCore;

namespace TestEFc_IEQable_AspNetCore01x02
{
public class TestHostedService:BackgroundService
{
private readonly ILogger<TestHostedService> _logger;
private readonly IServiceScopeFactory _serviceScopeFactory;
private int _executionCount;

public TestHostedService(ILogger<TestHostedService> logger, IServiceScopeFactory serviceScopeFactory)
{
_logger = logger;
_serviceScopeFactory = serviceScopeFactory;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("定时托管服务启动.");
await DoWork();
using PeriodicTimer timer = new(TimeSpan.FromSeconds(10));
try
{
while (await timer.WaitForNextTickAsync(stoppingToken))
{
await DoWork();
}
}
catch (OperationCanceledException)
{
_logger.LogInformation("定时托管服务已停止.");
}
}

private async Task DoWork()
{
using var scope = _serviceScopeFactory.CreateScope();
var dateContext = scope.ServiceProvider.GetRequiredService<DateContext>();

var toDoList = await dateContext.ToDoListTable
.Include(t => t.TransactionNodes)
.FirstOrDefaultAsync(x => x.Id == "1ca");
//这里忘了加判空了,有时候可能返回 null,但是也能用
_logger.LogInformation($"ToDoList Id:{toDoList.Id}, Title:{toDoList.ListTitle}, Description:{toDoList.ListDescription}, State:{toDoList.State}");

foreach (var item in toDoList.TransactionNodes)
{
_logger.LogInformation($"TransactionNode Id:{item.Id}, SerialNumber:{item.SerialNumber}, Content:{item.Content}, State:{item.State}");
}
int count = Interlocked.Increment(ref _executionCount);
await Task.Delay(TimeSpan.FromSeconds(2));
_logger.LogInformation("定时托管服务运行 {Count}", count);
}
}
}

运行截图:
hdhva