Service Worker还是Worker 服务?
- Service Worker 是一种在浏览器中运行后台任务的方法,如果您想在服务器上执行某些操作,则绝对不适合。
- Worker 服务本质上是一个模板,其中包含在控制台应用程序中运行 BackgroundService/IHostedService 以及(可选地,通过扩展)作为 Linux 守护程序或 Windows 服务所需的(少数)调用。您不需要该模板来创建和运行 BackgroundService。
ASP.NET Core 中托管服务的后台任务教程展示了如何创建和使用 BackgroundService,但有点……过度设计。这篇文章试图同时展示太多的东西,最终错过了一些重要的东西。
一个更好的介绍是史蒂夫戈登的什么是工人服务?.
后台服务
创建后台服务所需的只是一个实现IHostedService接口的类。与实现所有接口方法不同,从BackgroundService基类继承并仅覆盖该方法更容易ExecuteAsync
。
这篇文章的例子表明这个方法不需要太花哨:
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
public Worker(ILogger<Worker> logger)
{
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
await Task.Delay(1000, stoppingToken);
}
}
}
这只是一个有延迟的循环。这将一直运行,直到 Web 应用程序终止并发出stoppingToken
. 该服务将由 DI 容器创建,因此它可以像ILogger
或任何其他单例服务一样具有服务依赖项。
注册服务
后台服务需要注册为 中的服务ConfigureServices
,注册其他服务的方式相同。如果您有一个控制台应用程序,您可以在主机ConfigureServices
调用中对其进行配置。如果您有 Web 应用程序,则需要在以下位置注册Startup.ConfigureServices
:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<OrdersContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
...
//Register the service
services.AddHostedService<Worker>();
services.AddRazorPages();
}
这注册Worker
为可以由 DI 容器构建的服务,并将其添加到托管服务列表中,该服务将.Run()
在 Web 应用程序的调用中启动一次Main
:
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
使用 DbContext 和其他作用域服务
添加 DbContext 作为依赖项比较棘手,因为DbContext
它是一个作用域服务。我们不能只注入一个DbContext
实例并将其存储在一个字段中 - DbContext 旨在用作工作单元,它收集为单个场景所做的所有更改并将所有更改提交到数据库或丢弃它们。它应该在using
块内使用。如果我们处理我们注入的单个DbContext
实例,我们从哪里得到一个新实例?
为了解决这个问题,我们必须注入 DI 服务,IServiceProvider
显式地创建一个作用域并从这个作用域中获取我们的 DbContext:
public class Worker : BackgroundService
{
private readonly ILogger<Worker> _logger;
private readonly IServiceProvider _services;
//Inject IServiceProvider
public Worker(IServiceProvider services, ILogger<Worker> logger)
{
_logger = logger;
_services=services;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
//Create the scope
using (var scope = _services.CreateScope())
{
//Create OrdersContext in the scope
var ctx = scope.ServiceProvider.GetRequiredService<OrdersContext>();
var latestOrders = await ctx.Orders
.Where(o=>o.Created>=DateTime.Today)
.ToListAsync();
//Make some changes
if (allOK)
{
await ctx.SaveChangesAsync();
}
}
//OrdersContext will be disposed when exiting the scope
...
}
}
}
将OrdersContext
在范围退出时释放,并且任何未保存的更改都将被丢弃。
没有说整个代码需要在里面ExecuteAsync
。一旦代码开始变得太长,我们可以轻松地将重要代码提取到单独的方法中:
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
using (var scope = _services.CreateScope())
{
var ctx = scope.ServiceProvider.GetRequiredService<OrdersContext>();
await DoWorkAsync(ctx,stoppingToken);
}
await Task.Delay(1000, stoppingToken);
}
}
private async Task DoWorkAsync(OrdersContext ctx,CancellationToken stoppingToken)
{
var latestOrders = await ctx.Orders
.Where(o=>o.Created>=DateTime.Today)
.ToListAsync();
//Make some changes
if (allOK)
{
await ctx.SaveChangesAsync();
}
}