Custom Scheduled Tasks with C#Bot

This article explores how to create scheduled tasks.


C#Bot Custom Scheduled Tasks

Creating a task

The purpose of our scheduled task will be to run the TankManagementService (we wrote in the CsharpBot Custom Business Logic article) once a day, at 7am. This service checks maintains the Clean status of each tank in the database. Running the scheduled tasks each day means that the Clean status will be up to date each day.

To create a new task, first you will need to create a new file for it. Navigate to serverside/src/Services/Scheduling, creeate a folder called Tasks, and create a file called TankManagementTask.cs inside the Tasks folder.

Our TankManagementTask needs to extend the interface IScheduledTask and implement the interface members ExecuteAsync and Schedule in order to be run by the Scheduler in SchedulerHosdtedService.cs.

  • Schedule is a string that contains the Cron Expression used to tell our SchedulerHostedService when to run the scheduled tasks.
  • ExecuteAsync is the main method that is executed. This method is the entry point into the task and contains all the code that will be run when the task is schdeuled.

We will also register our task as a service in startup.cs so we are able to perform dependancy injection with our scheduled task. Since we want to be running the TankManagementService inside out task, we will also need to create a constructor that asks for this service to be injected.

Create your class to looks like the following.

using System.Threading;
using System.Threading.Tasks;

namespace Universesharp.Services.Scheduling.Tasks
{
    public class TankManagementTask : IScheduledTask
    {
        public string Schedule => "0 7 * * *";

        private readonly TankManagementService _tankManagementService;

        public TankManagementTask (TankManagementService tankManagementService)
        {
            _tankManagementService = tankManagementService;
        }

        public async Task ExecuteAsync(CancellationToken cancellationToken)
        {
            await _tankManagementService.UpdateTankStatus();
        }
    }
}

Configure task schedule

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

public string Schedule => "0 7 * * *";

Which translates to 7am each day.

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.

Register the task

Now that the task has been created and configured, we need to register the task in startup.cs so that the scheduler is aware that there is an IScheduledTask to be run, and also so that dependancy injection will work for our task.

Find the protected region [Add more scheduled task here], turn it on, and add the TankManagementTask so that it looks like the code below.

// % protected region % [Add more scheduled task here] on begin
services.TryAddSingleton<IScheduledTask, TankManagementTask>();
// % protected region % [Add more scheduled task here] end

You will also need to turn on the protected region at the top [Add any extra imports here] and import the namespace of your new scheduled task. This block will look like hte code below

// % protected region % [Add any extra imports here] on begin
using Universesharp.Services.Scheduling.Tasks;
// % protected region % [Add any extra imports here] end

That’s it! Your task should now run each day at 7am, and all of your tanks clean status will always be up to date!

Extra Information

C#Bot supports the creation of scheduled tasks to complete jobs at a specific time or recurring 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, meaning 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 you could do the same thing using a background thread, it would be killed when the application shuts down, and not cleaned up properly.

IHostedService is the entry point to code execution. The order of service registration in ConfigureServices controls what order each IHostedService is executed in. When the host starts, StartAsync is called for each IHostedService. By the same token, when the host shuts down gracefully StopAsync is called by the reverse registration order.

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 the other scheduled tasks inherited from IScheduledTask.
Once created, when the host starts, the main background task collects all the scheduled tasks which implement 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
    });
}

When the SchedulerHostedService.ExecuteAsync is run, it loops through the collection of scheduled tasks to manage and schedule them according to the crontab expression. The library we use for the expressions is the third party library NCrontab. It is primarily used for:

  • Parsing of cron expressions
  • Formatting of cron expressions
  • Calculation of occurrences of time based on a cron schedule

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 overridden to suit your own implementation needs. See the following example:

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 every minute with a minute delay. Then it will loop through all the tasks to check if any should be run (according to the cron expression), 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);
}

Last updated: 16 September 2020


Start modelling your app today.