Run scheduled background tasks in ASP.NET Core

In the previous blog post called background tasks with ASP.NET Core using the IHostedService Peter described how to use the IHostedInterface for background tasks. In this post, we continue on this subject and add some pointers on how to perform scheduled background tasks.

In many software projects, there are repetitive tasks; some do just repeat every x seconds after the last instance is finished but you might also have to run a task on a schedule like every 10 minutes. When building repeating or scheduled tasks there are many options on how to approach the scheduling and this approach can be influenced by a number of technical choices.

Building the scheduling yourself is an option when you do not want to add extra dependencies to your project, have full control or just want an extra technical challenge. An out of the box solution you can a look at Hangfire, Quartz.net, or an external service that does an http call every x seconds to trigger the task (something like Pingdom).

Authors

Michiel van Oudheusden

Microsoft .NET consultant, developer, architect. Focus on ALM, DevOps, APIs, Azure and everything around it. More about Michiel on his blog

Peter Groenewegen
.NET technologies, Azure, VSTS, Testing, delivering great software.

Let’s assume you are building the scheduling yourself because you can. In this blog post, we will give you some pointers on some pitfalls. You will have a good starting point to do your implementation.

Background processing for tasks

When running a background task in ASP.NET Core, the IHostedService gives you a good skeleton to build the scheduler logic. When using the hosted service, you do need to keep in mind to handle the dependency injection correctly. The IHostedService runs as a singleton for your task processing. When starting a scheduled task, the task has to be given an independent dependency injection scope. How to do this can you read in ASP.NET Core background processing. The ScopedProcessor class from this article is a good starting point for implementing a scheduled task. The important part of the dependency injection is the Process method:

protected override async Task Process()
{
    using (var scope = _serviceScopeFactory.CreateScope())
    {
        await ProcessInScope(scope.ServiceProvider);
    }
}

The implementation of the ProcessInScope method will run your logic. In the basic implementation, the ScopedProcessor class adds a 5-second delay between the processing of the task. Adding scheduling is the next step.

Scheduling the background task with Cron expression
A Cron expression is a format that let you specify when to trigger the next execution of your task. For example; 1 0 * * * will trigger 1 minute past midnight every day. A Cron expression enables you to precisely specify when to start a task.

┌───────────── minute (0 - 59)
│ ┌───────────── hour (0 - 23)
│ │ ┌───────────── day of month (1 - 31)
│ │ │ ┌───────────── month (1 - 12)
│ │ │ │ ┌───────────── day of week (0 - 6) (Sunday to Saturday;
│ │ │ │ │                                       7 is also Sunday on some systems)
│ │ │ │ │
│ │ │ │ │
* * * * *

When creating a base class for scheduled tasks, the ScopedProcessor is a good base class for the ScheduledProcessor. You have to override the ExecuteAsync to start processing based on the Cron expression. For parsing the Cron expression we use a standard library (nuget package NCrontab). This package can parse the Cron expression and determine the next run.

    public abstract class ScheduledProcessor : ScopedProcessor
    {
        private CrontabSchedule _schedule;
        private DateTime _nextRun;
        protected abstract string Schedule { get; }
public ScheduledProcessor(IServiceScopeFactory serviceScopeFactory) : base(serviceScopeFactory)
        {
            _schedule = CrontabSchedule.Parse(Schedule);
            _nextRun = _schedule.GetNextOccurrence(DateTime.Now);
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            do
            {
                var now = DateTime.Now;
                var nextrun = _schedule.GetNextOccurrence(now);
                if (now > _nextRun)
                {
                    await Process();
                    _nextRun = _schedule.GetNextOccurrence(DateTime.Now);
                }
                await Task.Delay(5000, stoppingToken); //5 seconds delay
            }
            while (!stoppingToken.IsCancellationRequested);
        }
    }

The next step is to implement the actual task that has to be scheduled. Inherit from the ScheduledProcessor and implement the Schedule and ProcessInScope method:

    public class ScheduleTask : ScheduledProcessor
    {
        public ScheduleTask(IServiceScopeFactory serviceScopeFactory) : base(serviceScopeFactory)
        {
        }

        protected override string Schedule => "*/10 * * * *"; //Runs every 10 minutes

        public override Task ProcessInScope(IServiceProvider serviceProvider)
        {
            Console.WriteLine("Processing starts here");
            return Task.CompletedTask;
        }
   }

The last step is to register your newly created class as a IHostedService in startup.cs.

     services.AddSingleton();

Now you are ready for the basic scenario where you only have one instance and do not need advanced monitoring and can miss some processing rounds. When the task runs longer than the interval between the scheduled moments, it will skip starting the process. Gracefully canceling a running task on shutdown, error handling and handling processing when the service was restarted or had downtime can also be improved.

Related posts
Background processing
Headless services
Using scoped services
Using HttpClientFactory

A working demo of a background process/scheduled background process can be found in the following git repository Demo code background processing with IHostedService.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.