如何在 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,这将处理预定的后台任务,管理服务器之间的平衡,并且具有很好的可扩展性。

https://www.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 支持这个。
  • 与石英相比,它不灵活。

这是根据之前的回复和

改进的版本

改进:

  1. 直到上一个任务执行完毕才会启动定时器。这将有助于避免出现两个任务同时执行的情况。
  2. 支持异步任务
  3. 它处理任务执行期间可能出现的异常,以确保它不会阻止下一个任务的执行。
  4. 为执行范围的每个任务创建一个范围,因此您可以访问 RunJobAsync 中的任何范围内的服务
  5. 可以在继承的class.
  6. 中指定间隔和初始任务执行时间

访问范围内的服务示例

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