如何在ASP.NET Core 2.1中定时运行BackgroundService

22
我想在ASP.NET Core 2.1中运行一个后台作业。它每2个小时运行一次,并且需要访问我的DI容器,因为它将在数据库中执行某些清理操作。它需要是async的,并且应该独立于我的ASP.NET Core 2.1应用程序运行。
我看到有一个IHostedService,但是ASP.NET Core 2.1还引入了一个抽象类BackgroundService,为我做了更多的工作。看起来不错,我想使用它!
虽然我没有能够找出如何在计时器上运行从BackgroundService派生的服务。
我需要在ExecuteAsync(token)中配置这个吗?通过记住上次运行的时间并确定是否为2小时,或者有更好/更清晰的方法只需在某个地方声明它必须每2小时运行?
此外,我的BackgroundService解决问题的方法正确吗?
谢谢!
编辑:
此帖发布在MS扩展存储库上。

9
文档中举例了一个“定时后台服务”。请查看“ASP.NET Core中托管服务的后台任务”以获取更多信息。链接如下:https://learn.microsoft.com/zh-cn/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-2.2#timed-background-tasks - Panagiotis Kanavos
嗯,我明白了。问题是,我发现DoWork()不是异步的。我可以将DoWork()标记为异步,但这并不是正确的方法,因为它不会被等待执行。 - S. ten Brinke
@PanagiotisKanavos 如果您有答案,请将其编写为实际答案,以便在您的答案帮助我解决此问题时,我可以将其标记为已完成 :) - S. ten Brinke
你能告诉我为什么实现IHostedService,然后使用计时器会比使用BackgroundService并通过计时器检查是否想要在ExecuteAsync中运行工作更好吗?(再次发表您的回答+为什么这种方法比此方法更好的原因)我知道我的方法会导致在不执行任务的情况下调用ExecuteAsync,但是我的问题是:如果不能将其放在计时器上,那么BackgroundService的意义何在?接着问:为什么没有一个TimedBackgroundService呢? - S. ten Brinke
对于这条评论串,我很抱歉,但我认为@PanagiotisKanavos所说的可能有效。评论中提到可以使用以下async代码实现定时器:Timer: var timer = new System.Threading.Timer(async (e) => { await Task.Delay(500); Console.WriteLine("Tick"); }, null, 0, 5000);然而,该线程说需要使用try/catch来管理异常。如果发生未处理的异常,计时器可能会停止工作。再次强调,这些应该作为实际答案提出,以便更容易地讨论。 - S. ten Brinke
2
注意使用带有计时器的托管服务,因为IIS每20分钟会进行回收,此时您的托管服务也将停止。因此,您需要将应用程序池设置为始终运行,这可能会导致泄漏或内存问题。 - Alexein1
4个回答

26

更新于2022年3月,详见底部!

更新于2020年4月,详见底部!

@Panagiotis Kanavos在我的问题评论中给出了答案,但未将其发布为实际答案;此回答专门针对他/她。

我使用了类似于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调用异步化。

未来可以编写一个扩展程序,将这样的类放在Extensions仓库中,因为我认为这非常有用。我在说明中发布了github问题链接。

提示:如果您计划将此类用于多个托管服务,请考虑创建包含定时器和抽象PerformWork()(或其他类似内容)的基类,以便“时间”逻辑仅存在于一个位置。

感谢您的回答!我希望这能帮助未来的某个人。

更新 04-2020

在正常的Core服务集合DI容器中,无法在此处注入作用域服务。我使用的是autofac,因为注册错误,所以可以在构造函数中使用作用域服务例如IClassRepository,但是当我开始处理另一个只使用AddScoped<>(),AddSingleton<>(),AddTransient<>()的项目时,我们发现注入作用域的东西不起作用,因为您不在范围的上下文中。

为了使用您的作用域服务,请注入IServiceScopeFactory(易于测试)并使用CreateScope(),这允许您在using语句中使用scope.GetService() :)

更新 03-2022

这篇文章得到了很多的浏览和关注,但我必须说我不再是我解决方案的粉丝了。我建议使用不同的解决方案:

  • 使用hangfire或quartz代替,如果您只想让代码在backgroundservice中运行
  • 如果您在kubernetes环境中运行,请查看kubernetes cronjobs
    • 这样做的好处是仅在需要时运行您的代码,与全天候运行项目并每天凌晨3点执行一次作业相比,可以节省资源
  • 请查看Azure Functions / AWS Lambda定时器
    • 这可能比制作自己的定时托管服务更便宜,更易于维护。但是,将其集成到k8s环境中可能更加困难。

此答案的缺点包括:

  • 您需要自己管理许多其他选项免费提供的东西。例如:
    • 如果应用程序在应该运行作业时关闭会怎么样?
    • 如果您的作业时间太长并且另一个作业开始了怎么办?
    • 记录和监视。
  • 我仍然不确定此解决方案中的async支持。我从未真正弄清楚是否正确。
  • 我也不喜欢DI没有默认支持。 Quartz.Net支持此功能。
  • 与quartz相比,它不够灵活。

2
这并不使计时器异步化,new Timer(async () => await DoWorkAsync() 默认情况下,计时器会按照定时计划执行您的函数,而不管其他函数是否仍在执行。另外,如果没有进行任何请求,也无法保证计时器会被执行,可以查看这个问题。我曾经对计时器有同样的误解,答案对此进行了解释。 - johnny 5
每个登录应用的用户,背景服务将首先触发,随后的触发时间在进程中指定。 - S2K
不要忘记在Startup.cs中使用“services.AddHostedService<TimedHostedService>();”注册服务。 - IngoB

4

实现这一点的方法之一是使用Hangfire.io,它可以处理计划的后台任务、管理服务器之间的负载均衡,并且具有良好的可扩展性。

请访问https://www.hangfire.io了解关于定期作业的更多信息。


5
我想补充一点,.NET Core提供了完全免费的解决方案。HangFire将根据您的使用情况收费。正如我在帖子中提到的,我想使用ASP.NET Core的解决方案,因为它已经存在;我只是不知道如何使用它。对于像这样的事情使用第三方解决方案似乎有点奇怪,因为我要做的并不复杂。 - S. ten Brinke
当然。如果您使用SQL Server,HangFire是免费的,并提供完整的解决方案,您可以实施并继续进行开发。(注:我与其无关)但我确实理解您的要求,只是想帮忙。 - Mark Redman
如果您使用SQL Server,HangFire是免费的。您能提供相关链接吗?此外,我很感谢您的回复,只是它并不是我想要使用的ASP.NET Core功能的一部分,因此我觉得它不应该成为帖子的答案。 :) - S. ten Brinke
在这里您可以查看 https://www.hangfire.io/pricing/,第一栏为“开源”,Hangfire Core采用LGPL 3.0协议发布,商业使用无妨。 - jkosmala

1

我已经找到了一个简单的解决方案,使用.NET Core内置的功能。它直接使用BackgroundService而不是IHostedServiceIDisposable。 我受到了Philipp Bauknecht的博客文章的启发:https://medium.com/medialesson/run-and-manage-periodic-background-tasks-in-asp-net-core-6-with-c-578a31f4b7a3 他使得服务可以被暂停,更加易于管理。

注意:当将此应用程序托管在IIS或Azure App Service中时,请确保该应用程序始终处于运行状态,否则托管服务将在一段时间后关闭。

public class TimedBackgroundService : BackgroundService
{
    private async Task ExecuteTaskAsync()
    {
        // ...
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        TimeSpan interval = TimeSpan.FromMinutes(60);
        // Available with .NET 6.0
        using PeriodicTimer timer = new PeriodicTimer(interval);

        while (!stoppingToken.IsCancellationRequested &&
            await timer.WaitForNextTickAsync(stoppingToken))
            {
                // Alternative to PeriodicTimer (available in .NET Core 2.1).
                // REMARK: Calls would successively be delayed a little bit this way.
                //await Task.Delay(interval, stoppingToken);

                await ExecuteTaskAsync();    
            }
    }
}

别忘了在Startup中注册它。

services.AddHostedService<TimedBackgroundService>

0

这是一个基于先前回答和https://dev59.com/6FQJ5IYBdhLWcg3wdFd4#56666084的改进版本。

改善措施:

  1. 在上一个任务执行完成之前不会启动计时器。这有助于避免两个任务同时执行的情况。
  2. 它支持异步任务。
  3. 它处理可能发生的任务执行异常,以确保它不会阻止下一个任务的执行。
  4. 对于每个执行的任务,都会创建一个作用域,因此您可以在RunJobAsync中访问任何作用域服务。
  5. 您可以在继承类中指定间隔和初始任务执行时间。

作用域服务访问示例:

    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();
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接