3

i need to trigger hosted service at certain time, eg. everyday at 13:00 o'clock. Mine typical ExecuteAsync method seems like that:

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
     var interval = _configuration.IntervalInSeconds * 1000;

     while (!stoppingToken.IsCancellationRequested)
     { 
         // some job

         await Task
             .Delay(interval, stoppingToken);
     }
}

When the application starts, the executeAsync method is called, and then some action is taking place every minute for example. Now i have to perform such an action only on certain hour. Is there any method to achieve such a behavior?? Is there any solution to my problem apart from rough calculating the next trigger time?? Thanks for any help.

Bulchsu
  • 580
  • 11
  • 33
  • 1
    You should probably use a library like Hangfire or Coravle for this. Timers (Task.Delay uses a timer too) are good for repeating events but not for schedules. What happens if your service restarts for some reason? The timer will restart – Panagiotis Kanavos Sep 22 '20 at 07:58
  • 1
    Suggested approach is more like: Hangfire, Quartz.NET or maybe Windows Scheduler – madoxdev Sep 22 '20 at 08:00
  • You could calculate the difference between the next scheduled time and now, and wait for that moment. Or you could create a timer directly with an initial delay equal to that time that repeats every 1 day, eg `_timer=new Timerr(DoWork,null,ScheduledTime-DateTime.Now,TimeSpan.FromDays(1))`. But what happens if the service goes down? – Panagiotis Kanavos Sep 22 '20 at 08:00
  • @404 that will get you the next execution time but not repeated every 1 day. Which is easy to do if a timer is used explicitly instead of `Task.Delay()`. The *real* problem is that 1 day is simply too long for a timer loop – Panagiotis Kanavos Sep 22 '20 at 08:03
  • You could simply add an `if` statement around `// some job`. Quick and dirty. – Aron Sep 22 '20 at 08:12

1 Answers1

10

By using the Timer method, you could run the task at a specific time, calculate the timespan between the current and target time and pass it as the dueTime parameter.

code as below:

    public Task StartAsync(CancellationToken cancellationToken)
    {
        //DateTime.Today: 00:00:00
        TimeSpan delayTime = DateTime.Today.AddHours(21) - DateTime.Now;
        TimeSpan intervalTime = TimeSpan.FromMinutes(1); //86400000s == 1 day
        _timer = new Timer(DoWork, null, delayTime, intervalTime);
        return Task.CompletedTask;
    }

Besides, the timer or Task.Delay() methods are more applied to executing a method at specified intervals, if you want to implement scheduled tasks, I suggest you could also try to use the Cronos package and the Cron Expressions to configure the scheduled task (reference: link).

The Cronos package is a lightweight but full-fledged library for parsing cron expressions and calculating next occurrences with time zones and daylight saving time in mind. The Cronos is an open source project sponsored by HangfireIO, and you can read detailed documentation from its GitHub repository. The details steps as below:

  1. Install the Cronos package via NuGet.

  2. Create a CronJobService service with the following code:

     public abstract class CronJobService : IHostedService, IDisposable
     {
         private System.Timers.Timer _timer;
         private readonly CronExpression _expression;
         private readonly TimeZoneInfo _timeZoneInfo;
    
         protected CronJobService(string cronExpression, TimeZoneInfo timeZoneInfo)
         {
             _expression = CronExpression.Parse(cronExpression);
             _timeZoneInfo = timeZoneInfo;
         }
    
         public virtual async Task StartAsync(CancellationToken cancellationToken)
         {
             await ScheduleJob(cancellationToken);
         }
    
         protected virtual async Task ScheduleJob(CancellationToken cancellationToken)
         {
             var next = _expression.GetNextOccurrence(DateTimeOffset.Now, _timeZoneInfo);
             if (next.HasValue)
             {
                 var delay = next.Value - DateTimeOffset.Now;
                 if (delay.TotalMilliseconds <= 0)   // prevent non-positive values from being passed into Timer
                 {
                     await ScheduleJob(cancellationToken);
                 }
                 _timer = new System.Timers.Timer(delay.TotalMilliseconds);
                 _timer.Elapsed += async (sender, args) =>
                 {
                     _timer.Dispose();  // reset and dispose timer
                     _timer = null;
    
                     if (!cancellationToken.IsCancellationRequested)
                     {
                         await DoWork(cancellationToken);
                     }
    
                     if (!cancellationToken.IsCancellationRequested)
                     {
                         await ScheduleJob(cancellationToken);    // reschedule next
                     }
                 };
                 _timer.Start();
             }
             await Task.CompletedTask;
         }
    
         public virtual async Task DoWork(CancellationToken cancellationToken)
         {
             await Task.Delay(5000, cancellationToken);  // do the work
         }
    
         public virtual async Task StopAsync(CancellationToken cancellationToken)
         {
             _timer?.Stop();
             await Task.CompletedTask;
         }
    
         public virtual void Dispose()
         {
             _timer?.Dispose();
         }
     }
    
     public interface IScheduleConfig<T>
     {
         string CronExpression { get; set; }
         TimeZoneInfo TimeZoneInfo { get; set; }
     }
    
     public class ScheduleConfig<T> : IScheduleConfig<T>
     {
         public string CronExpression { get; set; }
         public TimeZoneInfo TimeZoneInfo { get; set; }
     }
    
     public static class ScheduledServiceExtensions
     {
         public static IServiceCollection AddCronJob<T>(this IServiceCollection services, Action<IScheduleConfig<T>> options) where T : CronJobService
         {
             if (options == null)
             {
                 throw new ArgumentNullException(nameof(options), @"Please provide Schedule Configurations.");
             }
             var config = new ScheduleConfig<T>();
             options.Invoke(config);
             if (string.IsNullOrWhiteSpace(config.CronExpression))
             {
                 throw new ArgumentNullException(nameof(ScheduleConfig<T>.CronExpression), @"Empty Cron Expression is not allowed.");
             }
    
             services.AddSingleton<IScheduleConfig<T>>(config);
             services.AddHostedService<T>();
             return services;
         }
     }
    
  3. create a ScheduleJob.cs:

     public class ScheduleJob: CronJobService
     {
         private readonly ILogger<ScheduleJob> _logger;
    
         public ScheduleJob(IScheduleConfig<ScheduleJob> config, ILogger<ScheduleJob> logger)
             : base(config.CronExpression, config.TimeZoneInfo)
         {
             _logger = logger;
         }
    
         public override Task StartAsync(CancellationToken cancellationToken)
         {
             _logger.LogInformation("ScheduleJob starts.");
             return base.StartAsync(cancellationToken);
         }
    
         public override Task DoWork(CancellationToken cancellationToken)
         {
             _logger.LogInformation($"{DateTime.Now:hh:mm:ss} ScheduleJob is working.");
             return Task.CompletedTask;
         }
    
         public override Task StopAsync(CancellationToken cancellationToken)
         {
             _logger.LogInformation("ScheduleJob is stopping.");
             return base.StopAsync(cancellationToken);
         }
     }
    
  4. Register the ScheduleJob service in the ConfigureServices method.

     public void ConfigureServices(IServiceCollection services)
     {
         services.AddHostedService<HelloWorldHostedService>(); 
    
         services.AddCronJob<ScheduleJob>(c=>
         {
             c.TimeZoneInfo = TimeZoneInfo.Local;
             c.CronExpression = @"25 21 * * *"; // 21:25 PM daily.
         });
    
         services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
     }
    

    Then the result as below:

    enter image description here

Zhi Lv
  • 18,845
  • 1
  • 19
  • 30
  • How do you set up the scheduled job to run only once? For example, a job is scheduled to run in 5 minutes. After the job is executed, it should stop and should no longer run. – Jeonsoft FaceBundy Mar 17 '21 at 03:16
  • On the line `if (delay.TotalMilliseconds <= 0)` does it need to return in that if block? Otherwise, it recalls itself until it has a positive value, but the negative from the first iteration will eventually get set when it unwinds and throw an exception. (other than that it works great). – b.pell May 02 '22 at 13:33