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.
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?
LikeLike
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.
LikeLike
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 ?
LikeLike
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.
LikeLike
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.
LikeLike
You can find a working sample for 2.1 at github: https://github.com/pgroene/ASPNETCoreScheduler. The BackgroundService is a custom implementation of the IHostedService interface.
LikeLike
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.
LikeLike
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
LikeLike
Have you used the code from the GitHub sample: https://github.com/pgroene/ASPNETCoreScheduler. From experience, I know that is working. How do you do the scheduling of the exact time, it seems that your problem is somewhere in there.
LikeLike
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 * * *”;
LikeLike
I think you have to use utc time.
LikeLike
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.
LikeLike
You can start multiple tasks in the ExecuteAsync method. The cron expression will just trigger at which time the method is called.
LikeLiked by 1 person
Hi
Thanks for your suggestion. Can you please give small example ?.
LikeLike
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!
LikeLike
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.
LikeLike
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.
LikeLiked by 1 person
whats the right way in your example code to stop the scheduletask, change the schedule and start the scheduletask again?
LikeLike
How to run scheduler with a service account ?
Under application pool , which we could with regular windows service
LikeLike
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 ?
LikeLike
Hi again, If I have several tasks, do I have to register (AddHostedService) them one by one?
LikeLike
Yes, you have to register each.
LikeLike
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
LikeLike
That depends on what you want to do. So, I’m not able to help you without context.
LikeLike
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
LikeLike
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?
LikeLike
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
LikeLike
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
LikeLiked by 1 person
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?
LikeLike
Scratch that, mistake on my part. This is awesome, thanks!
LikeLike
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?
LikeLike
Sorry for the late reaction. I do not think using a shorter Delay will add a lot of overhead. I really depends on what you think is acceptable for what you are doing.
LikeLike