Как в ASP.NET Core 3.1 запланировать фоновую задачу (задания Cron) с размещенными службами на определенную дату и время в будущем?

Я работаю над проектом на основе ASP.NET Core 3.1, и я хочу добавить к нему определенные функции, чтобы запланировать публикацию сообщения в будущем в дату и время, указанные автором сообщения (что-то вроде того, что Wordpress делает для запланированных сообщений через его задания cron). Например, если мы получим эту дату и время от пользователя:

2020-09-07 14:08:07

Тогда как я могу запланировать для него фоновую задачу, используя размещенные службы для запуска только один раз, изменить флаг в базе данных и сохранить изменения после этого?

Я читал несколько статей об этом, но они не указывали дату и время, а просто упоминали повторяющиеся задачи каждые 5 секунд и тому подобное с выражениями cron, но мне нужно знать, как я могу планировать фоновую задачу на определенную дату и время?

Заранее спасибо.


person Aspian    schedule 08.09.2020    source источник
comment
Я не понимаю, как можно избежать периодической проверки. Вы можете абстрагировать его с помощью Rx или чего-то в этом роде.   -  person Aluan Haddad    schedule 08.09.2020
comment
FluentScheduler — один из лучших github.com/fluentscheduler/FluentScheduler.   -  person Roman Ryzhiy    schedule 08.09.2020
comment
Альтернатива: hangfire.io   -  person Christoph Lütjen    schedule 08.09.2020
comment
Я бы рекомендовал использовать специально разработанное решение для этой функциональности, такое как Hangfire или подобное. С ними довольно легко работать.   -  person trademark    schedule 08.09.2020


Ответы (4)


Я объединил 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;
}
person Paulius Raila    schedule 06.01.2021
comment
Небольшое объяснение вашего кода может иметь большое значение... - person surajs1n; 06.01.2021
comment
Почти такой же подход объясняется здесь: medium.com/@gtaposh/ - person Dejan; 31.03.2021
comment
@Dejan действительно, я прочитал это. но я решил не опросить, а подождать. - person Paulius Raila; 08.04.2021

Я хочу добавить к нему определенную функциональность, чтобы запланировать публикацию сообщения в будущем в дату и время, указанные автором сообщения. Например, если мы получим эту дату и время от пользователя: 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 Эмулятор — это инструмент, который эмулирует службы очередей Azure и т. д. для целей локальной разработки и тестирования. Вы можете попробовать протестировать код для служб хранилища локально, не создавая подписку Azure и не неся никаких затрат.

person Fei Han    schedule 09.09.2020
comment
Спасибо за ваш очень подробный ответ, и на самом деле было бы здорово, если бы я использовал службы очереди сообщений. Но все же по какой-то причине я должен использовать для этого что-то вроде System.Threading.Timer, и с Timespan я думаю, что нашел способ сделать это. Я очень ценю ваш ответ и описание. - person Aspian; 09.09.2020

Используйте 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);
        });
person Omid Rafiee    schedule 08.09.2020
comment
Фу. Это выглядело великолепно, но было объявлено устаревшим в пользу... DNTCommon.Web.Core, который делает все виды вещей, которые мне не нужны, с большим количеством зависимостей. - person Sören Kuklau; 24.03.2021

После некоторых проб и ошибок я нашел способ запланировать фоновую задачу на определенную дату и время с помощью размещенной службы, как я задал в вопросе, и я сделал это с 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 .

Подробнее о фоновых задачах с размещенными службами в ASP.NET Core можно прочитать здесь, в официальных документах Microsoft:

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

person Aspian    schedule 10.09.2020
comment
Но он сбрасывает все таймеры после перезапуска приложения, верно? - person Mateech; 31.03.2021
comment
Да, но в методе ScheduledItemChangeState вы можете хранить все задачи в таблице базы данных и после перезапуска проверяет, не наступило ли время запуска какой-либо задачи, и запускает их, а после запуска удаляет их из таблицы. Так что вы все равно не потеряете запланированные задачи. - person Aspian; 31.03.2021
comment
@Mateech Вы можете увидеть полную реализацию, которая имеет возможность восстанавливать запланированные задачи из базы данных после перезапуска сервера, в моем репозитории Github по следующей ссылке: github.com/aspian-io/aspian/tree/master/Infrastructure/Schedule - person Aspian; 31.03.2021