Service worker 或 Worker service?
- Service Worker 是一种在浏览器中运行后台任务的方式,如果您想在服务器上执行某些操作,则绝对不适合。
- Worker 服务本质上是一个模板,其中包含在控制台应用程序中运行 BackgroundService/IHostedService 所需的(少数)调用,以及(可选地,通过扩展)作为 Linux 守护程序或 Windows 服务。您不需要该模板来创建和运行 BackgroundService。
教程Background tasks with hosted services in ASP.NET Core 展示了如何创建和使用BackgroundService,但有点……过度设计。这篇文章试图同时展示太多东西,结果却遗漏了一些重要的东西。
更好的介绍是 Steve Gordon 的 What are Worker Services?。
后台服务
创建后台服务所需的只是一个实现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 容器构造的服务并将其添加到托管服务列表中,一旦在Web 应用程序的@987654337 中调用.Run() @:
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();
}
}