在ASP.NET Core 3.1中,我如何使用托管服务安排一个特定日期和时间的后台任务(Cron Jobs)?

22

我正在开发一个基于ASP.NET Core 3.1的项目,想要为它添加一个特定的功能,即在由文章作者指定的日期和时间安排一篇文章的发布(类似于WordPress通过其cron jobs安排计划发布的文章)。例如,如果我们从用户那里收到以下日期和时间:

2020-09-07 14:08:07

那么,我该如何使用托管服务调度后台任务仅运行一次并更改数据库中的标志并保存更改?

我已经阅读了一些相关文章,但它们没有指定日期和时间,只是提到了每5秒执行重复任务等与cron表达式有关的内容,但是我需要知道的是如何按照特定日期和时间安排后台任务?

提前感谢您。


我不认为你能避免定期检查。你可以使用 Rx 或类似的东西进行抽象化。 - Aluan Haddad
3
FluentScheduler 是其中之一最好的 https://github.com/fluentscheduler/FluentScheduler - Roman Ryzhiy
2
备选方案:https://www.hangfire.io/ - Christoph Lütjen
我建议使用像Hangfire或类似的专门解决此功能的解决方案。它们非常易于使用。 - trademark
这里是答案 https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/fundamentals/host/hosted-services/samples - NoWar
4个回答

16

我将CrontabSchedule与IHostedService相结合。下面的实现是轻量级的(没有强制架构的库)并且没有轮询。

public class SomeScheduledService: IHostedService
{
    private readonly CrontabSchedule _crontabSchedule;
    private DateTime _nextRun;
    private const string Schedule = "0 0 1 * * *"; // run day at 1 am
    private readonly SomeTask _task;

    public SomeScheduledService(SomeTask task)
    {
        _task = Task;
        _crontabSchedule = CrontabSchedule.Parse(Schedule, new CrontabSchedule.ParseOptions{IncludingSeconds = true});
        _nextRun = _crontabSchedule.GetNextOccurrence(DateTime.Now);
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        Task.Run(async () =>
        {
            while (!cancellationToken.IsCancellationRequested)
            {
                await Task.Delay(UntilNextExecution(), cancellationToken); // wait until next time

                await _task.Execute(); //execute some task

                _nextRun = _crontabSchedule.GetNextOccurrence(DateTime.Now);
            }
        }, cancellationToken);

        return Task.CompletedTask;
    }

    private int UntilNextExecution() => Math.Max(0, (int)_nextRun.Subtract(DateTime.Now).TotalMilliseconds);

    public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

5
您的代码稍作解释,可以大有裨益... - surajs1n
3
这里解释了与此类似的方法:https://medium.com/@gtaposh/net-core-3-1-cron-jobs-background-service-e3026047b26d - Dejan
@Dejan 确实,我已经阅读了它。但我选择等待而不是轮询。 - Paulius Raila
@PauliusRaila Visual Studio建议添加NCrontab.Signed包?这是你的代码使用的库吗? - T3.0
@T3.0已经过去了一段时间,但它看起来像是这个:https://github.com/atifaziz/NCrontab - Paulius Raila
如果你的任务可以快速完成,获取下一个发生时间可能会得到与当前时间相同的日期时间,导致任务执行两次。为了解决这个问题,你需要将上次发生时间与当前时间进行比较,并选择较大的那个: _nextRun = _crontabSchedule.GetNextOccurrence(_nextRun > DateTime.Now ? _nextRun : DateTime.Now); - undefined

11
经过一些尝试,我找到了一种使用托管服务在特定日期和时间计划后台任务的方法,就像我在问题中询问的那样,并且,我用 System.Threading.TimerTimespan 进行了操作,例如:
public class ScheduleTask : IScheduler, IDisposable
{

   private Timer _timer;
   private IBackgroundTaskQueue TaskQueue { get; }

   // Set task to schedule for a specific date and time
    public async Task SetAndQueueTaskAsync(ScheduleTypeEnum scheduleType, DateTime scheduleFor, Guid scheduledItemId)
    {
        // Omitted for simplicity
        // ....

        TaskQueue.QueueBackgroundWorkItem(SetTimer);
    }

   // ......
   // lines omitted for simplicity
   // .....

   // Set timer for schedule item
   private Task SetTimer(CancellationToken stoppingToken)
   {
      // ......
      // lines omitted for simplicity
      // .....

      _timer = new Timer(DoWork, null, (item.ScheduledFor - DateTime.UtcNow).Duration(), TimeSpan.Zero);


      return Task.CompletedTask;
   }

   private void DoWork(object state)
   {
       ScheduledItemChangeState(DateTime.UtcNow).Wait();
   }

   // Changes after the scheduled time comes
   private async Task ScheduledItemChangeState(DateTime scheduleFor)
   {
       using (var scope = Services.CreateScope())
       {
           var context =
            scope.ServiceProvider
                .GetRequiredService<DataContext>();

          // Changing some data in database
       }
    }

   public void Dispose()
   {
      _timer?.Dispose();
   }
}

如果您查看上述代码的部分,在其中我将 (item.ScheduledFor - DateTime.UtcNow) 作为 Timer 类构造函数的第三个参数传递以初始化一个新实例,实际上是要求计时器在我存储为 DateTime 的特定时间 item.ScheduledFor 中执行特定的工作。
您可以从 Microsoft 官方文档中了解有关在 ASP.NET Core 中使用托管服务的后台任务的更多信息。

https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio

要查看完整的实现,请使用以下链接访问我的Github repo,它具有在重新启动服务器后从数据库中恢复定时任务的可能性

https://github.com/aspian-io/aspian/tree/master/Infrastructure/Schedule


1
但是重启应用程序后,它会丢失所有计时器,对吗? - Mateech
是的,但在“ScheduledItemChangeState”方法中,您可以将所有任务存储在数据库表中,重启后它会检查运行任何任务的时间,并运行它们,在运行后从表中删除它们。因此,您无论如何都不会失去预定的任务。 - Aspian
1
@Mateech 你可以通过以下链接在我的Github仓库中查看完整的实现,该实现具有在重新启动服务器后从数据库中恢复计划任务的可能性:https://github.com/aspian-io/aspian/tree/master/Infrastructure/Schedule - Aspian

3
我想为它添加一个特定的功能,以便在由帖子作者指定的日期和时间安排将来发布帖子。例如,如果我们从用户那里收到这个日期和时间:2020-09-07 14:08:07。
那么,我该如何使用托管服务安排后台任务,只运行一次并更改数据库中的标志并保存更改?
看起来你想在用户指定的日期时间执行后台任务/作业。为实现此需求,你可以尝试使用一些消息队列服务,例如Azure Queue Storage,它使我们能够通过设置visibilityTimeout来指定消息对Dequeue和Peek操作不可见的时间。
当应用程序用户想要创建一个新帖子并指定发布日期时间时,你可以将一条新消息(具有基于用户期望日期时间指定的visibilityTimeout)插入到队列中,这样插入的新消息就只会在队列中指定的日期时间可见。
QueueClient theQueue = new QueueClient(connectionString, "mystoragequeue");

if (null != await theQueue.CreateIfNotExistsAsync())
{
    //The queue was created...
}

var newPost = "Post Content Here";

var user_specified_datetime = new DateTime(2020, 9, 9, 20, 50, 25);
var datetime_now = DateTime.Now;

TimeSpan duration = user_specified_datetime.Subtract(datetime_now);

await theQueue.SendMessageAsync(newPost, duration, default); 

然后,您可以实现一个队列触发的后台任务,以从队列中检索消息并更新数据库记录。
注意:Microsoft Azure Storage Emulator 是一种工具,用于模拟本地开发和测试目的的Azure队列等服务,您可以尝试在本地测试代码而无需创建Azure订阅或产生任何费用。

谢谢您的非常详细的回答,如果我使用消息队列服务,那将是非常棒的。但是由于某些原因,我仍然需要使用类似 System.Threading.Timer 这样的东西,并且通过 Timespan,我想我已经找到了一种方法来做到这一点。我非常感谢您的回答和说明。 - Aspian

1
使用DNTScheduler并设置特定日期和时间。
        services.AddDNTScheduler(options =>
        {
            // DNTScheduler needs a ping service to keep it alive. Set it to false if you don't need it. Its default value is true.
            // options.AddPingTask = false;

            options.AddScheduledTask<DoBackupTask>(
                runAt: utcNow =>
                {
                    var now = utcNow.AddHours(3.5);
                    return  now.Hour == 14 && now.Minute == 08 && now.Second == 07;
                },
                order: 1);
        });

4
啊,看起来很不错,但已被淘汰,推荐使用DNTCommon.Web.Core,它做了各种我不需要的事情,并且有很多依赖项。 - Sören Kuklau

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