14

我正在开发一个基于 ASP.NET Core 3.1 的项目,我想向它添加一个特定的功能,以安排将来在帖子作者指定的日期和时间发布帖子(类似于 Wordpress 通过它的 cron 工作)。例如,如果我们从用户那里收到这个日期和时间:

2020-09-07 14:08:07

那么,如何通过使用托管服务只运行一次并更改数据库中的标志并在此之后保存更改来为其安排后台任务?

我读过一些关于它的文章,但他们没有指定日期和时间,只是提到了每 5 秒重复的任务以及类似 cron 表达式的东西,但是,我需要知道的是如何安排后台任务特定日期和时间?

先感谢您。

4

4 回答 4

13

我将 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;
}
于 2021-01-06T06:11:13.943 回答
4

经过一些试验和错误后,我找到了一种方法,通过使用托管服务来安排特定日期和时间的后台任务,正如我在问题中所问的那样,我这样做是System.Threading.Timer这样的Timespan

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://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio

要查看我的 Github 存储库中的完整实现,它可以在重新启动服务器后从数据库中恢复计划任务,请使用以下链接:

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

于 2020-09-10T11:00:33.587 回答
2

我想向它添加一个特定的功能,以安排将来在帖子作者指定的日期和时间发布帖子。例如,如果我们从用户那里收到此日期和时间:2020-09-07 14:08:07 .

那么,如何通过使用托管服务只运行一次并更改数据库中的标志并在此之后保存更改来为其安排后台任务?

看来您想在用户指定的日期时间执行后台任务/作业,为了达到要求,您可以尝试使用一些消息队列服务,例如Azure Queue Storage,它使我们能够指定消息多长时间应该通过设置对 Dequeue 和 Peek 操作不可见visibilityTimeout

当您的应用程序用户想要创建新帖子并指定发布日期时间时,您可以将新消息(具有基于用户预期日期时间的指定 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 Queue 等服务用于本地开发和测试目的的工具,您可以尝试在本地针对存储服务测试代码,而无需创建 Azure 订阅或产生任何费用。

于 2020-09-09T12:57:53.353 回答
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);
        });
于 2020-09-08T14:40:23.497 回答