In ASP.NET Core 3.1, how can I schedule a background task (Cron Jobs) with hosted services for a specific date and time in the future?
Asked Answered
M

4

24

I am working on a project based on ASP.NET Core 3.1 and I want to add a specific functionality to it to schedule publishing a post in the future in a date and time specified by post author (something like what Wordpress does for scheduled posts through its cron jobs). For example, if we receive this date and time from user :

2020-09-07 14:08:07

Then, how can I schedule a background task for it by using hosted services to run only for one time and to change a flag in database and save changes after that?

I've read some articles about it but they didn't specify date and time and just mentioned repeated tasks for every 5 second and stuff like that with cron expressions, but, the thing I need to know is how can I schedule a background task for a specific date and time?

Thank you in advance.

Mascia answered 8/9, 2020 at 13:49 Comment(5)
I don't see how you can avoid periodic checking. You can abstract it with Rx or something like thatCrutch
FluentScheduler is one of the best github.com/fluentscheduler/FluentSchedulerFibrinolysin
Alternative : hangfire.ioAssert
I'd recommend using a purpose-built solution for this functionality like Hangfire or similar. They're pretty easy to work with.Rask
Here is the answer github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/…Piero
M
11

After some trial and error I found a way to schedule a background task for specific date and time by using hosted service as I asked in the question, and, I did that with System.Threading.Timer and Timespan like this:

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

If you look at the part of the above code in which I passed (item.ScheduledFor - DateTime.UtcNow) as Timer class constructor's third parameter to initialize a new instance of it, I actually ask the timer to do a specific work in a specific time I stored as a DateTime in item.ScheduledFor.

You could read more about background tasks with hosted services in ASP.NET Core here from official Microsoft docs:

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

To see the full implementation in my Github repo which has the possibility to recover the scheduled tasks from database after restarting the server, use the following link:

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

Mascia answered 10/9, 2020 at 11:0 Comment(3)
But it drops all timers after restarting application, right?Density
Yes, but in "ScheduledItemChangeState" method, you can store all tasks in a database table and after restarting, it checks to see if time of running any task comes and runs them and, after running, delete them from the table. So you won't lose scheduled tasks anyway.Mascia
@Density You can see the full implementation which has the possibility to recover the scheduled tasks from database after restarting the server, in my Github repo through the following link: github.com/aspian-io/aspian/tree/master/Infrastructure/ScheduleMascia
G
16

I combined CrontabSchedule with IHostedService. The implementation below is lightweight (no architecture imposing libs) and no polling.

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;
}
Gray answered 6/1, 2021 at 6:11 Comment(6)
A little explanation of your code can go long way...Communism
The pretty much same approach is explained here: medium.com/@gtaposh/…Babbette
@Babbette indeed, i read it. but I chose not to poll but await instead.Gray
@PauliusRaila visual studio suggested adding the Package NCrontab.Signed? is this the library your code is using?Iridize
@Iridize was a while back. but it looks like the one: github.com/atifaziz/NCrontabGray
If your task can be completed quickly, getting the next occurrence time may get the same datetime as this one, causing the task to execute twice. To solve this, you need to compare the last occurrence time with current time and pick the max one: _nextRun = _crontabSchedule.GetNextOccurrence(_nextRun > DateTime.Now ? _nextRun : DateTime.Now);Trunnel
M
11

After some trial and error I found a way to schedule a background task for specific date and time by using hosted service as I asked in the question, and, I did that with System.Threading.Timer and Timespan like this:

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

If you look at the part of the above code in which I passed (item.ScheduledFor - DateTime.UtcNow) as Timer class constructor's third parameter to initialize a new instance of it, I actually ask the timer to do a specific work in a specific time I stored as a DateTime in item.ScheduledFor.

You could read more about background tasks with hosted services in ASP.NET Core here from official Microsoft docs:

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

To see the full implementation in my Github repo which has the possibility to recover the scheduled tasks from database after restarting the server, use the following link:

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

Mascia answered 10/9, 2020 at 11:0 Comment(3)
But it drops all timers after restarting application, right?Density
Yes, but in "ScheduledItemChangeState" method, you can store all tasks in a database table and after restarting, it checks to see if time of running any task comes and runs them and, after running, delete them from the table. So you won't lose scheduled tasks anyway.Mascia
@Density You can see the full implementation which has the possibility to recover the scheduled tasks from database after restarting the server, in my Github repo through the following link: github.com/aspian-io/aspian/tree/master/Infrastructure/ScheduleMascia
C
3

I want to add a specific functionality to it to schedule publishing a post in the future in a date and time specified by post author.For example, if we receive this date and time from user : 2020-09-07 14:08:07 .

Then, how can I schedule a background task for it by using hosted services to run only for one time and to change a flag in database and save changes after that?

It seems that you'd like to execute a background task/job at a user specified datetime, to achieve the requirement, you can try to use some message queue services, such as Azure Queue Storage, which enable us to specify how long the message should be invisible to Dequeue and Peek operations by setting visibilityTimeout.

While your application user want to create a new post and specify a publishing date time, you can insert a new message (with specified visibilityTimeout based on user expected datetime) into the queue, so that this new inserted message would be only visible at specified date time in the queue.

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

Then you can implement a queue triggered background task to retrieve message(s) from the queue and update your database record(s).

Note: Microsoft Azure Storage Emulator is a tool that emulates the Azure Queue etc services for local development and testing purposes, you can try to test code against the storage services locally without creating an Azure subscription or incurring any costs.

Cellist answered 9/9, 2020 at 12:57 Comment(1)
Thank you for your very thorough answer and actually it would be awesome If I were to use message queue services. But still for some reason I have to use something like System.Threading.Timer for that and with Timespan I guess I found a way to do that. I really appreciate your answer and description.Mascia
C
1

Use DNTScheduler and set specific date and time

        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);
        });
Clytemnestra answered 8/9, 2020 at 14:40 Comment(1)
Ugh. That looked great, but was deprecated in favor of… DNTCommon.Web.Core, which does all kinds of things I don't need, with a lot of dependencies.Sparkman

© 2022 - 2024 — McMap. All rights reserved.