Developer Docs

C#Bot: Custom Scheduled Tasks

Getting started

In this article we are going to create a custom scheduled task. A scheduled task is a piece of code that runs on a set interval in the background of the application. Scheduled tasks are excellent for tasks such as periodic data maintenance.

For this project, we will be continuing with the LMS Example Project repository.

User Story : As a Content Manager I want the readers to be aware if content has not been updated in the past month.

Acceptance Criteria :

  1. If the Last Modified Date was greater than 30 days ago (approx 1 month), the Summary must be edited to warn readers that the content may be out of date.

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

Creating the task

Our OutdatedArticleTask does not need to extend any specific class, and we can specify any method to be called on our scheduled task interval. We are going to make a method called UpdateOutdatedArticles that will be 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 scheduled.

Scheduled tasks can request all services that have been registered for dependency injection in the application, and a new service scope is created for each task run so it is safe to resolve scoped services.
In this case we want to use the LmssharpDBContext inside our task, so we will need to create a constructor that asks for this service to be injected.

Create your class to looks like the following.

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Lmssharp.Models;
using Microsoft.EntityFrameworkCore;

namespace Lmssharp.Services.Tasks
{
    public class OutdatedArticleTask
    {
        private const string OutdatedMessage = " Some of the content in this article may be out of date.";

        private readonly LmssharpDBContext _dbContext;

        public OutdatedArticleTask(LmssharpDBContext dbContext)
        {
            // Retrieve a dbContext from dependency injection and assign it to a class variable
            _dbContext = dbContext;
        }

        public async Task UpdateOutdatedArticles(CancellationToken cancellationToken = default)
        {
            // Fetch the articles that have old modified timestamps and not marked as outdated
            var oneMonthAgo = DateTime.Now.Subtract(new TimeSpan(30, 0, 0, 0));
            var outdatedArticles = await _dbContext.ArticleEntity
                .Where(x => x.Modified < oneMonthAgo)
                .Where(x => !x.Summary.Contains(OutdatedMessage))
                .ToListAsync(cancellationToken);

            // Update the article descriptions
            foreach (var outdatedArticle in outdatedArticles)
            {
                outdatedArticle.Summary += OutdatedMessage;
            }

            // Save back to the database
            await _dbContext.SaveChangesAsync(cancellationToken);
        }
    }
}

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 scheduled task to run in the application.

Find the protected region [Add extra core scoped services here], turn it on, and add the OutdatedArticleTask so that it looks like the code below. This will register the task with the dependency injection container.

// % protected region % [Add extra core scoped services here] on begin
services.TryAddScoped<OutdatedArticleTask>();
// % protected region % [Add extra core scoped services 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 Lmssharp.Services.Tasks;
// % protected region % [Add any extra imports here] end

Finally you will need to turn on the protected region labelled [Add methods before data seeding here] and register the with the scheduler.

// % protected region % [Add methods before data seeding here] on begin
RecurringJob.AddOrUpdate<OutdatedArticleTask>(
    "Update Outdated Articles", // A unique id for the scheduled task
    o => o.UpdateOutdatedArticles(default), // The function to run
    "0 * * * *"); // The crontab to run (explained below)
// % protected region % [Add methods before data seeding here] end

You may notice that we pass in default for the cancellation token. Hangfire will substitute that for the actual cancellation token when running the task, we are just passing in a placeholder argument.

That’s it! Your task should now run at the start of every hour!

Cron scheduling

When we were registering the task one of the fields used in the registration was a crontab. Crontab was originally created for the unix cron utility as a way to specify when repeating tasks will run.

A crontab expression has the following structure.

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

These fields can be set to a number to represent the timing of when the task should be run. In the case of our task we had set the following.

"0 * * * *"

So this will translate to On the 0th minute of every hour. Crontab guru is an excellent tool for debugging when crontabs will be evaluated.

Extra information

C#Bot implements it’s scheduled task framework by using Hangfire. Because of this you get access to all of the advanced functionality that Hangfire provides.

Multi instance support

In production environments it is not uncommon to have multiple copies of a single application running for the purposes of redundancy and load balancing. One common issue that occurs in these environments is that without any coordination a scheduled task could run on all nodes at the same time when it should really only be running on one.

Thanks to Hangfire this issue is solved out of the box. When a server takes a job off the queue to run it will register a lock in the database to specify that it is being executed. This means that if another server polls the database to see what tasks can be executed it will detect that the task is already being run and not execute it twice.

Dashboard

Hangfire comes with a built in dashboard feature as well that can be used to visualise statistics on the execution of scheduled jobs. This can be navigated to from the admin dashboard of your application or by going directly to the /api/hangfire url.

Removing old tasks

Since the tasks that are to be run are stored in the database, this means that if you no longer want a task to execute removing the registration from the code is not enough. In the [Add methods before data seeding here] protected region of Startup.cs you can add the following code to remove the old task.

RecurringJob.RemoveIfExists("Update Outdated Articles");

Retries

If a scheduled task fails (by throwing an exception) then by default Hangfire will attempt to retry the task up to 10 times. These retries will stop once the task has succeeded and can be seen on the dashboard. This behaviour can be controlled using the AutomaticRetry on your task method. More details can be found on the Hangfire exceptions documentation.