如何在 ASP.NET Core 2.1 的定时器上 运行 BackgroundService
How to run BackgroundService on a timer in ASP.NET Core 2.1
我想 运行 ASP.NET Core 2.1 中的后台作业。它必须每 2 小时 运行 并且它需要访问我的 DI 容器,因为它将在数据库中执行一些清理。它需要 async
并且应该 运行 独立于我的 ASP.NET Core 2.1 应用程序。
我看到有一个 IHostedService
,但是 ASP.NET Core 2.1 还引入了一个名为 BackgroundService
的抽象 class,它为我做了更多的工作。看起来不错,我想用那个!
不过,我无法弄清楚 运行 一个从 BackgroundService
派生的服务如何在计时器上运行。
我是否需要在 ExecuteAsync(token)
中通过记住上次 运行 并确定这是否是 2 小时来配置它,或者是否有 better/cleaner 方法来仅在某处说它必须每 2 小时 运行?
此外,我解决问题的方法是否正确 BackgroundService
?
谢谢!
编辑:
将此张贴在 MS extensions repo
实现此目的的一种方法是使用 HangFire.io,这将处理预定的后台任务,管理服务器之间的平衡,并且具有很好的可扩展性。
查看重复性工作
03-2022更新,在底部阅读!
04-2020更新,在底部阅读!
@Panagiotis Kanavos 在我的问题的评论中给出了答案,但它并没有 post 作为实际答案;此答案献给 him/her.
我使用 Timed background service 类似于 Microsoft 文档中的那个来创建服务。
internal class TimedHostedService : IHostedService, IDisposable
{
private readonly ILogger _logger;
private Timer _timer;
public TimedHostedService(ILogger<TimedHostedService> logger)
{
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Timed Background Service is starting.");
_timer = new Timer(DoWork, null, TimeSpan.Zero,
TimeSpan.FromSeconds(5));
return Task.CompletedTask;
}
private void DoWork(object state)
{
_logger.LogInformation("Timed Background Service is working.");
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Timed Background Service is stopping.");
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
}
在我的例子中,我通过 new Timer(async () => await DoWorkAsync(), ...)
.
使 _timer
调用异步
将来,可以编写一个扩展,使 class 在扩展库中可用,因为我认为这非常有用。我在描述中 post 编辑了 github 问题 link。
提示,如果您计划将此 class 重复用于多个托管服务,请考虑创建一个包含计时器和抽象 PerformWork()
的基础 class 或诸如此类的“时间”的逻辑只有一处。
感谢您的回答!我希望这对以后的人有所帮助。
更新 04-2020:
开箱即用的普通核心服务集合 DI 容器无法在此处注入范围内的服务。我正在使用 autofac,由于注册错误,它可以在构造函数中使用像 IClassRepository
这样的作用域服务,但是当我开始处理另一个只使用 AddScoped<>(), AddSingleton<>(), AddTransient<>()
的项目时,我们发现注入作用域的东西不工作,因为你不在范围内。
为了使用您的范围服务,注入一个 IServiceScopeFactory
(更易于测试)并使用 CreateScope()
,这样您就可以将 scope.GetService()
与 using
一起使用声明:)
更新 03-2022:
这个 post 获得了很多意见和关注,但我不得不说我不再是我的解决方案的忠实拥护者。我会提出不同的解决方案:
- 如果您希望代码只是 运行 in backgroundservice
,请改用 hangfire 或 quartz
- 如果您 运行 在 kubernetes 环境中,请查看 kubernetes cronjobs
- 这样的好处是仅在需要时 运行 运行您的代码,与 运行 运行项目 24/7 和仅在每天凌晨 3 点执行作业相比节省资源,例如
- 看看 Azure Functions/AWS 计时器上的 Lambda
- 与制作您自己的定时托管服务相比,这可能更便宜且更易于维护。不过,可能更难集成到 k8s 环境中。
此答案中 post 解决方案的缺点是:
- 您需要自己管理很多其他选项可以免费完成的事情。例如:
- 如果您的应用在本应 运行 作业时出现故障怎么办?
- 如果您的工作时间太长而另一份开始了怎么办?
- 记录和监控
- 我仍然不确定此解决方案中的
async
支持。我从来没有真正弄清楚这个解决方案是否“正确”
- 我也不喜欢不支持开箱即用的 DI。
Quartz.Net
支持这个。
- 与石英相比,它不灵活。
这是根据之前的回复和
改进的版本
改进:
- 直到上一个任务执行完毕才会启动定时器。这将有助于避免出现两个任务同时执行的情况。
- 支持异步任务
- 它处理任务执行期间可能出现的异常,以确保它不会阻止下一个任务的执行。
- 为执行范围的每个任务创建一个范围,因此您可以访问 RunJobAsync 中的任何范围内的服务
- 可以在继承的class.
中指定间隔和初始任务执行时间
访问范围内的服务示例
protected override async Task RunJobAsync(IServiceProvider serviceProvider, CancellationToken stoppingToken)
{
DbContext context = serviceProvider.GetRequiredService<DbContext>();
}
源代码:
public abstract class TimedHostedService : IHostedService, IDisposable
{
private readonly ILogger _logger;
private Timer _timer;
private Task _executingTask;
private readonly CancellationTokenSource _stoppingCts = new CancellationTokenSource();
IServiceProvider _services;
public TimedHostedService(IServiceProvider services)
{
_services = services;
_logger = _services.GetRequiredService<ILogger<TimedHostedService>>();
}
public Task StartAsync(CancellationToken cancellationToken)
{
_timer = new Timer(ExecuteTask, null,FirstRunAfter, TimeSpan.FromMilliseconds(-1));
return Task.CompletedTask;
}
private void ExecuteTask(object state)
{
_timer?.Change(Timeout.Infinite, 0);
_executingTask = ExecuteTaskAsync(_stoppingCts.Token);
}
private async Task ExecuteTaskAsync(CancellationToken stoppingToken)
{
try
{
using (var scope = _services.CreateScope())
{
await RunJobAsync(scope.ServiceProvider, stoppingToken);
}
}
catch (Exception exception)
{
_logger.LogError("BackgroundTask Failed", exception);
}
_timer.Change(Interval, TimeSpan.FromMilliseconds(-1));
}
/// <summary>
/// This method is called when the <see cref="IHostedService"/> starts. The implementation should return a task
/// </summary>
/// <param name="serviceProvider"></param>
/// <param name="stoppingToken">Triggered when <see cref="IHostedService.StopAsync(CancellationToken)"/> is called.</param>
/// <returns>A <see cref="Task"/> that represents the long running operations.</returns>
protected abstract Task RunJobAsync(IServiceProvider serviceProvider, CancellationToken stoppingToken);
protected abstract TimeSpan Interval { get; }
protected abstract TimeSpan FirstRunAfter { get; }
public virtual async Task StopAsync(CancellationToken cancellationToken)
{
_timer?.Change(Timeout.Infinite, 0);
// Stop called without start
if (_executingTask == null)
{
return;
}
try
{
// Signal cancellation to the executing method
_stoppingCts.Cancel();
}
finally
{
// Wait until the task completes or the stop token triggers
await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken));
}
}
public void Dispose()
{
_stoppingCts.Cancel();
_timer?.Dispose();
}
}
我想 运行 ASP.NET Core 2.1 中的后台作业。它必须每 2 小时 运行 并且它需要访问我的 DI 容器,因为它将在数据库中执行一些清理。它需要 async
并且应该 运行 独立于我的 ASP.NET Core 2.1 应用程序。
我看到有一个 IHostedService
,但是 ASP.NET Core 2.1 还引入了一个名为 BackgroundService
的抽象 class,它为我做了更多的工作。看起来不错,我想用那个!
不过,我无法弄清楚 运行 一个从 BackgroundService
派生的服务如何在计时器上运行。
我是否需要在 ExecuteAsync(token)
中通过记住上次 运行 并确定这是否是 2 小时来配置它,或者是否有 better/cleaner 方法来仅在某处说它必须每 2 小时 运行?
此外,我解决问题的方法是否正确 BackgroundService
?
谢谢!
编辑:
将此张贴在 MS extensions repo
实现此目的的一种方法是使用 HangFire.io,这将处理预定的后台任务,管理服务器之间的平衡,并且具有很好的可扩展性。
查看重复性工作03-2022更新,在底部阅读!
04-2020更新,在底部阅读!
@Panagiotis Kanavos 在我的问题的评论中给出了答案,但它并没有 post 作为实际答案;此答案献给 him/her.
我使用 Timed background service 类似于 Microsoft 文档中的那个来创建服务。
internal class TimedHostedService : IHostedService, IDisposable
{
private readonly ILogger _logger;
private Timer _timer;
public TimedHostedService(ILogger<TimedHostedService> logger)
{
_logger = logger;
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Timed Background Service is starting.");
_timer = new Timer(DoWork, null, TimeSpan.Zero,
TimeSpan.FromSeconds(5));
return Task.CompletedTask;
}
private void DoWork(object state)
{
_logger.LogInformation("Timed Background Service is working.");
}
public Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Timed Background Service is stopping.");
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
}
在我的例子中,我通过 new Timer(async () => await DoWorkAsync(), ...)
.
_timer
调用异步
将来,可以编写一个扩展,使 class 在扩展库中可用,因为我认为这非常有用。我在描述中 post 编辑了 github 问题 link。
提示,如果您计划将此 class 重复用于多个托管服务,请考虑创建一个包含计时器和抽象 PerformWork()
的基础 class 或诸如此类的“时间”的逻辑只有一处。
感谢您的回答!我希望这对以后的人有所帮助。
更新 04-2020:
开箱即用的普通核心服务集合 DI 容器无法在此处注入范围内的服务。我正在使用 autofac,由于注册错误,它可以在构造函数中使用像 IClassRepository
这样的作用域服务,但是当我开始处理另一个只使用 AddScoped<>(), AddSingleton<>(), AddTransient<>()
的项目时,我们发现注入作用域的东西不工作,因为你不在范围内。
为了使用您的范围服务,注入一个 IServiceScopeFactory
(更易于测试)并使用 CreateScope()
,这样您就可以将 scope.GetService()
与 using
一起使用声明:)
更新 03-2022: 这个 post 获得了很多意见和关注,但我不得不说我不再是我的解决方案的忠实拥护者。我会提出不同的解决方案:
- 如果您希望代码只是 运行 in backgroundservice ,请改用 hangfire 或 quartz
- 如果您 运行 在 kubernetes 环境中,请查看 kubernetes cronjobs
- 这样的好处是仅在需要时 运行 运行您的代码,与 运行 运行项目 24/7 和仅在每天凌晨 3 点执行作业相比节省资源,例如
- 看看 Azure Functions/AWS 计时器上的 Lambda
- 与制作您自己的定时托管服务相比,这可能更便宜且更易于维护。不过,可能更难集成到 k8s 环境中。
此答案中 post 解决方案的缺点是:
- 您需要自己管理很多其他选项可以免费完成的事情。例如:
- 如果您的应用在本应 运行 作业时出现故障怎么办?
- 如果您的工作时间太长而另一份开始了怎么办?
- 记录和监控
- 我仍然不确定此解决方案中的
async
支持。我从来没有真正弄清楚这个解决方案是否“正确” - 我也不喜欢不支持开箱即用的 DI。
Quartz.Net
支持这个。 - 与石英相比,它不灵活。
这是根据之前的回复和
改进:
- 直到上一个任务执行完毕才会启动定时器。这将有助于避免出现两个任务同时执行的情况。
- 支持异步任务
- 它处理任务执行期间可能出现的异常,以确保它不会阻止下一个任务的执行。
- 为执行范围的每个任务创建一个范围,因此您可以访问 RunJobAsync 中的任何范围内的服务
- 可以在继承的class. 中指定间隔和初始任务执行时间
访问范围内的服务示例
protected override async Task RunJobAsync(IServiceProvider serviceProvider, CancellationToken stoppingToken)
{
DbContext context = serviceProvider.GetRequiredService<DbContext>();
}
源代码:
public abstract class TimedHostedService : IHostedService, IDisposable
{
private readonly ILogger _logger;
private Timer _timer;
private Task _executingTask;
private readonly CancellationTokenSource _stoppingCts = new CancellationTokenSource();
IServiceProvider _services;
public TimedHostedService(IServiceProvider services)
{
_services = services;
_logger = _services.GetRequiredService<ILogger<TimedHostedService>>();
}
public Task StartAsync(CancellationToken cancellationToken)
{
_timer = new Timer(ExecuteTask, null,FirstRunAfter, TimeSpan.FromMilliseconds(-1));
return Task.CompletedTask;
}
private void ExecuteTask(object state)
{
_timer?.Change(Timeout.Infinite, 0);
_executingTask = ExecuteTaskAsync(_stoppingCts.Token);
}
private async Task ExecuteTaskAsync(CancellationToken stoppingToken)
{
try
{
using (var scope = _services.CreateScope())
{
await RunJobAsync(scope.ServiceProvider, stoppingToken);
}
}
catch (Exception exception)
{
_logger.LogError("BackgroundTask Failed", exception);
}
_timer.Change(Interval, TimeSpan.FromMilliseconds(-1));
}
/// <summary>
/// This method is called when the <see cref="IHostedService"/> starts. The implementation should return a task
/// </summary>
/// <param name="serviceProvider"></param>
/// <param name="stoppingToken">Triggered when <see cref="IHostedService.StopAsync(CancellationToken)"/> is called.</param>
/// <returns>A <see cref="Task"/> that represents the long running operations.</returns>
protected abstract Task RunJobAsync(IServiceProvider serviceProvider, CancellationToken stoppingToken);
protected abstract TimeSpan Interval { get; }
protected abstract TimeSpan FirstRunAfter { get; }
public virtual async Task StopAsync(CancellationToken cancellationToken)
{
_timer?.Change(Timeout.Infinite, 0);
// Stop called without start
if (_executingTask == null)
{
return;
}
try
{
// Signal cancellation to the executing method
_stoppingCts.Cancel();
}
finally
{
// Wait until the task completes or the stop token triggers
await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken));
}
}
public void Dispose()
{
_stoppingCts.Cancel();
_timer?.Dispose();
}
}