×
Back to book

Custom Scheduled Tasks with C#Bot

This article explores how to create scheduled tasks.

C#Bot supports the creation of scheduled tasks to complete jobs at a specific time or frequency.

For this, we implement a HostedService on IHostedService which is the base class of the service classes which can be registered as a background task.

The basic idea of IHostedService is it allows tasks to be registered as background tasks so that they can run in the background, and are coordinated with the lifetime of the application. Tasks are registered when the application starts and have the opportunity to do some graceful clean-up when the application is shutting down. While we could spin off work on a background thread previously, it would be killed when the main application process shutdown.
IHostedService is the entry point to code execution. Each IHostedService implementation is executed in the order of service registration in ConfigureServices. StartAsync is called on each IHostedService when the host starts. By the same token, StopAsync is called in reverse registration order when the host shuts down gracefully.

From the HostedService, a child class referred to as SchedulerHostedService can be created to function as the main background task for starting and managing all other scheduled tasks inherited from IScheduledTask.
Once created, when the host starts, the main background task collects all the scheduled tasks which implements IScheduledTask. This is illustrated with the following code snippet.

var referenceTime = DateTime.UtcNow;

foreach (var scheduledTask in scheduledTasks)
{
    _scheduledTasks.Add(new SchedulerTaskWrapper
    {
        Schedule = CrontabSchedule.Parse(scheduledTask.Schedule),
        Task = scheduledTask,
        NextRunTime = referenceTime
    });
}

So when the SchedulerHostedService.ExecuteAsync is run, it loops through these collection of scheduled tasks to manage and schedule them.

  • A third party library NCrontab is used for:
    • Parsing of crontab expressions
    • Formatting of crontab expressions
    • Calculation of occurrences of time based on a crontab schedule

Configuration

Customize tasks

In the folder serverside/src/Services/Scheduling/Tasks, create a new file for a task (referring to the existing SomeTask.cs), which has similar structure as the following:

public class SomeTask : IScheduledTask
{
    // For the cron job string formating, please refer to this  https://github.com/atifaziz/NCrontab/wiki/Crontab-Expression
    public string Schedule => "*/1 * * * *";

    public async Task ExecuteAsync(CancellationToken cancellationToken)
    {
        // Do your job here
        await Task.Delay(1000, cancellationToken);
        Console.Write("SomeTask just run once");
    }
}

Configure task schedule

For now, the task is managed by itself which is just the property Schedule in the example:

public string Schedule => "*/1 * * * *";

For full details on how to format and configure a task schedule, go to NCrontab.

This library does not provide any scheduler or a scheduling facility like cron from Unix platforms. What it provides is parsing, formatting, and an algorithm to produce occurrences of time based on a given schedule expressed in the crontab format.

* * * * *
- - - - -
| | | | |
| | | | +----- day of week (0 - 6) (Sunday=0)
| | | +------- month (1 - 12)
| | +--------- day of month (1 - 31)
| +----------- hour (0 - 23)
+------------- min (0 - 59)

or a six-part format that allows for seconds:

* * * * * *
- - - - - -
| | | | | |
| | | | | +--- day of week (0 - 6) (Sunday=0)
| | | | +----- month (1 - 12)
| | | +------- day of month (1 - 31)
| | +--------- hour (0 - 23)
| +----------- min (0 - 59)
+------------- sec (0 - 59)

For now our scheduled task only supports 'crontab format' which doesn't support second.

How does the scheduled tasks run

The scheduled tasks run when the HostedService runs at the start of the host. Since the SchedulerHostedService is inherited from HostedService, which is an implementation of IHostedService, the StartAsync in HostedService runs when the host starts:

public Task StartAsync(CancellationToken cancellationToken)
{
    // Create a linked token so we can trigger cancellation outside of this token's cancellation
    _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

    // Store the task we're executing
    _executingTask = ExecuteAsync(_cts.Token);

    // If the task is completed then return it, otherwise it's running
    return _executingTask.IsCompleted ? _executingTask : Task.CompletedTask;
}

The ExecuteAsync in HostedService is a protected abstract task that can be overrided as shown below to suit your own implementation needs.

protected abstract Task ExecuteAsync(CancellationToken cancellationToken);
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
    while (!cancellationToken.IsCancellationRequested)
    {
        await ExecuteOnceAsync(cancellationToken);
        await Task.Delay(TimeSpan.FromMinutes(1), cancellationToken);
    }
}

It has a while loop which will run ExecuteOnceAsync repeated with every 1 minute delay. Then it will loop through all the tasks to check if any should be run (according to the schedule configuration), and run them one by one. After calling ExecuteAsync on each of the tasks , the next running time for the task to rerun will be incremented (according to the schedule configuration) and ready for the next call.

foreach (var taskThatShouldRun in tasksThatShouldRun)
{
    taskThatShouldRun.Increment();

    await taskFactory.StartNew(
        async () =>
        {
            try
            {
                await taskThatShouldRun.Task.ExecuteAsync(cancellationToken);
            }
            catch (Exception ex)
            {
                var args = new UnobservedTaskExceptionEventArgs(
                    ex as AggregateException ?? new AggregateException(ex));

                UnobservedTaskException?.Invoke(this, args);

                if (!args.Observed)
                {
                    throw;
                }
            }
        },
        cancellationToken);
}

Related Articles