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.

15 thoughts on “Run scheduled background tasks in ASP.NET Core”

  1. Thanks for the great explanation and example for the IHostedService usage in ASP.NET Core 2, I have one question.

    Would I be correct in assuming that I can use the ScopeFactory to access my configuration service, in order to make the ScheduledTask schedule configurable?

    Like

    1. As always it depends… I do not know what you exactly want to read from your configuration, depending on what you need and if it updates while running or if you want to be able to inject it for testing. I would idd create a separate scope to read the configuration. Changing a running schedule would probably a challenge.

      Like

  2. This code is interesting but doesn’t seem to be current (or even current as of ASP.NET Core 2.1). If I’m reading it correctly, it depends on an implementation of BackgroundService other than the one provided in the framework as of 2.1. To be specific, the delivered BackgroundService doesn’t have a Process() method to override, which leads me to a dead end.

    An updated post on how to do this using the delivered BackgroundService in 2.1+ (assuming it’s possible!) would be really helpful.

    Like

      1. Thanks, yes I noticed that. I stopped short of trying it when I realized that there is an out of box “BackgroundService” class now that this code doesn’t implement. I can’t find any examples of this sort of scheduled processing that uses the delivered code – I assumed because these posts were written before that delivered code existed – but perhaps there is some other reason why one has to make their own implementation of BackgroundService to schedule services? Thanks.

        Like

  3. Its not sending on time which i have already mentioned time in scheduler task. Its sending in delay than already scheduled time. Mail sending is working fine but its not in proper time. I have configured all steps like above . Could you please help me . Thanks

    Like

      1. Yes, I am using the code from GitHub sample only. I am scheduling the time for every day in ScheduleTask file like below.
        protected override string Schedule => “01 14 * * *”;

        Like

  4. I wanted to send multiple ExecuteAsync(multiple task mails) instead of single mail for same scheduled Task datatime . How can i do that using cron expression . please give any idea.

    Like

      1. If you want to send multiple emails in parallel you can use the following code pattern in ExecuteAsync:

        var task1 = DoWorkAsync();
        var task2 = DoMoreWorkAsync();

        await Task.WhenAll(task1, task2);

        Like

  5. Hi Peter,

    Thanks for your code. I’m new to .Net Core and MVC (using EF) and i’m trying to execute some background tasks for my db.
    Could you point me in the right direction as to how I can get my db context within a scheduled task so I can use LINQ queries and execute actions against my db?

    Your code works fine when using API calls but I would prefer to use my db context instead.
    Thanks!

    Like

    1. The dbcontext is normally registered as a scoped service. That means that you have to be in a dependency scope to create one and it got the same lifetime as the scope it is created in. In the following blog post: https://thinkrethink.net/2018/07/12/injecting-a-scoped-service-into-ihostedservice/ you can read how to create your dbcontext.
      The method ProcessInScope in the scheduler creates a dependency scope where you can inject the dbcontext. When you a very long running scope of use a lot a multi-threading, then you can also create a factory method where you create the dbcontext. However, you have to manage the lifetime of the object your self. If done not correctly it can lead to memory leaks or situations where you use the object when it is no longer available.

      Like

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.