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.
We can use Quartz scheduling to perform a job every 5 minutes. To begin, make a project with the ASP.NET core web application template. Choose Asp.net MVC for your online application. There are two ways to install the Quartz package.
BackgroundService is a base class for implementing a long running IHostedService. ExecuteAsync(CancellationToken) is called to run the background service. The implementation returns a Task that represents the entire lifetime of the background service.
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
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;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With