繁体   English   中英

如何在 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的抽象类,它为我做了更多的工作。 看起来不错,我想用它!

不过,我一直无法弄清楚如何在计时器上运行从BackgroundService派生的服务。

我是否需要在ExecuteAsync(token)中配置它,方法是记住它上次运行的时间并确定这是否是 2 小时,或者是否有更好/更清晰的方法来说明它必须每 2 小时运行一次?

另外,我对BackgroundService问题的处理方法是否正确?

谢谢!

编辑:

将此发布在MS 扩展回购协议

03-2022更新,在底部阅读!

04-2020更新,在底部阅读!

@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调用异步。

将来,可以编写一个扩展,使这样的类在扩展存储库中可用,因为我认为这非常有用。 我在描述中发布了 github 问题链接。

提示,如果您计划为多个托管服务重用此类,请考虑创建一个包含计时器和抽象PerformWork()之类的基类,以便“时间”逻辑仅在一个地方。

谢谢您的回答。 我希望这对将来的人有帮助。

2020 年 4 月更新

开箱即用的普通核心服务集合 DI 容器无法在此处注入范围内的服务。 我正在使用 autofac,由于注册错误,它使得在构造函数中使用像IClassRepository这样的作用域服务成为可能,但是当我开始处理一个只使用AddScoped<>(), AddSingleton<>(), AddTransient<>()我们发现注入作用域的东西是行不通的,因为你不在作用域上下文中。

为了使用您的作用域服务,注入一个IServiceScopeFactory (更易于测试)并使用CreateScope() ,它允许您使用带有using语句的scope.GetService() :)

03-2022 更新:这篇文章获得了很多观点和关注,但我不得不说我不再是我的解决方案的忠实拥护者。 我会提出不同的解决方案:

  • 如果您希望代码仅在后台服务中运行,请改用 hangfire 或 quartz
  • 如果您在 kubernetes 环境中运行,请查看 kubernetes cronjobs
    • 这样做的好处是仅在需要时运行您的代码,与 24/7 全天候运行项目和每天凌晨 3 点仅执行作业相比节省资源,例如
  • 在计时器上查看 Azure Functions/AWS Lambda
    • 与制作您自己的定时托管服务相比,这可能更便宜且更易于维护。 不过,集成到 k8s 环境中可能更困难。

此答案中发布的解决方案的缺点是:

  • 您需要自己管理很多其他选项可以免费完成的事情。 例如:
    • 如果您的应用程序在本应运行该作业时出现故障怎么办?
    • 如果您的工作时间太长而另一个开始了怎么办?
    • 记录和监控
  • 我仍然不确定此解决方案中的async支持。 我从来没有真正弄清楚这个解决方案是否“正确”
  • 我也不喜欢开箱即用的不支持 DI。 Quartz.Net确实支持这一点。
  • 与石英相比,它不灵活。

实现这一目标的一种方法是使用 HangFire.io,这将处理预定的后台任务,管理跨服务器的平衡并且具有相当大的可扩展性。

请参阅https://www.hangfire.io上的经常性工作

这是基于以前的响应和https://stackoverflow.com/a/56666084/1178572的改进版本

改进:

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

我找到了一个使用 .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>

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM