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.

32 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

      1. I am not sure if i understood his question correctly, but I have similar question. if I want this task schedule to be configurable via app settings. For example if tomorrow my task should start every day at 4 pm instead of 10 PM, then i should be able to change just this value in configuration and I am good to go. Is it possible ?

        Like

      2. Yes that should be possible. The implementation will be a little different because you have to monitor your configuration every x minutes and when it changes restart your cron jobs.

        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

  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

      1. 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);
        }

        Can you explain me the purpose of the line: var nextrun = _schedule.GetNextOccurrence(now);
        Because I don’t see you using “nextrun” anywhere. Thank you.

        Liked by 1 person

  6. whats the right way in your example code to stop the scheduletask, change the schedule and start the scheduletask again?

    Like

  7. public ScheduleTask(IServiceScopeFactory serviceScopeFactory, IOptions emailSetting) : base(serviceScopeFactory)
    {
    _emailSetting = emailSetting.Value;
    }
    …..
    }
    i am using configuration settings by constructor dependency injection, On configuration change it doesn’t reflects public override Task ProcessInScope(IServiceProvider serviceProvider)
    {
    ….
    }
    in this method
    how i can get updated configuration ?

    Like

  8. Hi Peter, forgive my ignorance, but I have a question.
    What is the meaning of the delay and what is the best value for the delay
    Thank you

    Like

  9. I have the following code
    public class ScheduleTask : ScheduledProcessor
    {

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

    //protected override string Schedule => “*/10 * * * *”;
    protected override string Schedule => “*/2 * * * *”;

    public override Task ProcessInScope(IServiceProvider serviceProvider)
    {
    var orderClient = serviceProvider.GetService();
    Console.WriteLine(“Processing starts here”);
    return Task.CompletedTask;
    }
    }

    in startup I have code to register OrderHttpClient
    services.AddScoped();

    However, I always get null for
    var orderClient = serviceProvider.GetService();

    I find it only works for DbContext, the following code works.
    using (var context = serviceProvider.GetService())

    In our scheduled task, we need to call some service to do some job beyond the DbContext.

    Do you have any solution?

    Thanks
    Di Yin

    Like

    1. I would guess that you have a type miss match. If you register a service as scoped, you can only create it in a scope. If your dbcontext is scoped, you prove your scope is working. Are you sure you register the type and/or interface correctly?

      Like

      1. Yes, However I have figure out what is the problem, for any other services created by Us, we should use interface to get it by the following code

        var orderClient = serviceProvider.GetService();

        but not

        var orderClient = serviceProvider.GetService();

        Here the service is
        public interface IOrderHttpClient
        {
        Task DoSomething();
        }
        public class OrderHttpClient : IOrderHttpClient
        {
        public Task DoSomething()
        {
        // Do something here.
        }
        }

        Thanks a lot. The Post is very helpful.

        Thanks
        Di Yin

        Like

  10. The code in the comment lost the type after GetService by Akismet,
    The
    var orderClient = serviceProvider.GetService(); Here is for IOrderHttpClient
    but not
    var orderClient = serviceProvider.GetService(); Here is for OrderHttpClient

    Thanks
    Di Yin

    Liked by 1 person

  11. I followed your code from Github but I’m getting a dependency injection error. I’m not sure what the correct line would be for .NET Core 2.2 but neither of these seem to work.

    services.AddSingleton();
    OR
    services.AddHostedService();

    Any help?

    Like

  12. hi Peter, thanks for the awesome code here. I have a question, for a task that runs only once per day, what is the best delay time for checking? Is there any concern here? In your code, by default the task delayed every 5 seconds which means it will check like every 5 seconds to decide whether the task should run, do you think it is kinda inefficiency? btw is there a big over head for Task.Delay?

    Like

Leave a reply to Raj1990 Cancel reply

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