ASP.NET Core 2.2: The standalone service for scheduling jobs
Development | создано: 12/8/2019 | опубликовано: 12/8/2019 | обновлено: 11/14/2022 | просмотров: 2661 | всего комментариев: 0
The implementation of the IHostedService with cron-format scheduling and scope-operations.
What is it?
The implementation of the IHostedService with cron-format scheduling and scope-operations.
Base classes
The first base class is BackgroundHostedService that implements a base functionality for IHostedService:
/// <summary> /// Background service (hosted) /// </summary> public abstract class BackgroundHostedService : IHostedService { private Task _executingTask; private readonly CancellationTokenSource _stoppingCancellationTokenSource = new CancellationTokenSource(); public virtual Task StartAsync(CancellationToken cancellationToken) { _executingTask = ExecuteAsync(_stoppingCancellationTokenSource.Token); return _executingTask.IsCompleted ? _executingTask : Task.CompletedTask; } public virtual async Task StopAsync(CancellationToken cancellationToken) { if (_executingTask == null) { return; } try { _stoppingCancellationTokenSource.Cancel(); } finally { await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken)); } } protected virtual async Task ExecuteAsync(CancellationToken token) { do { await Process(token); await Task.Delay(5000, token); } while (!token.IsCancellationRequested); } protected abstract Task Process(CancellationToken token); }
Next, I used a previous base class to implement a ScopedBackgroundHostedService. As you can see it depending on IServiceScopeFactory.
/// <summary> /// Background processor with services' scope /// </summary> public abstract class ScopedBackgroundHostedService : BackgroundHostedService { private readonly IServiceScopeFactory _serviceScopeFactory; /// <inheritdoc /> protected ScopedBackgroundHostedService(IServiceScopeFactory serviceScopeFactory, ILogger logger) { _serviceScopeFactory = serviceScopeFactory; Logger = logger; } #region properties /// <summary> /// Current service name /// </summary> public string ServiceName => $"[{GetType().Name.ToUpperInvariant()}]"; /// <summary> /// Represents instance of the active logger /// </summary> protected ILogger Logger { get; } #endregion protected override async Task Process(CancellationToken token) { try { using (var scope = _serviceScopeFactory.CreateScope()) { Logger.LogInformation($"{ServiceName}: Start executing work at {DateTime.Now}"); await ProcessInScope(scope.ServiceProvider, token); } } catch (Exception exception) { Logger.LogError(exception, GetType().Name); throw; } } /// <summary> /// Process in scope /// </summary> /// <param name="serviceProvider"></param> /// <param name="token"></param> /// <returns></returns> protected abstract Task ProcessInScope(IServiceProvider serviceProvider, CancellationToken token); }
The Process method creates a scope for background operations. And the last base class for background operations is CrontabScheduledBackgroundHostedService. You make sure that you install NuGet-package for this class (read as "project")
Install-Package ncrontab -Version 3.3.1
The full text of that class is:
/// <summary> /// Scheduled Scoped Background Service with CronTab functionality /// * * * * * * /// | | | | | | /// | | | | | +--- day of week (0 - 6) (Sunday=0) /// | | | | +----- month (1 - 12) /// | | | +------- day of month (1 - 31) /// | | +--------- hour (0 - 23) /// | +----------- min (0 - 59) /// +------------- sec (0 - 59) /// </summary> public abstract class CrontabScheduledBackgroundHostedService : ScopedBackgroundHostedService { private CrontabSchedule _schedule; private DateTime _nextRun; protected abstract string Schedule { get; } protected CrontabScheduledBackgroundHostedService(IServiceScopeFactory serviceScopeFactory, ILogger logger) : base(serviceScopeFactory, logger) { GetSchedule(); } #region Properties /// <summary> /// Indicates that hosted service should start process on server restart /// </summary> protected virtual bool IsExecuteOnServerRestart => false; /// <summary> /// Identify service by name /// </summary> protected abstract string DisplayName { get; } #endregion private void GetSchedule() { if (string.IsNullOrEmpty(Schedule)) { throw new PricePointArgumentNullException(nameof(Schedule)); } _schedule = CrontabSchedule.Parse(Schedule); var currentDateTime = DateTime.Now; if (IsExecuteOnServerRestart) { _nextRun = currentDateTime.AddSeconds(5); Logger.LogInformation($"{DisplayName} ({nameof(IsExecuteOnServerRestart)} = {IsExecuteOnServerRestart})"); } else { _nextRun = _schedule.GetNextOccurrence(currentDateTime); } } protected override async Task ExecuteAsync(CancellationToken token) { do { var now = DateTime.Now; if (now > _nextRun) { await Process(token); _nextRun = _schedule.GetNextOccurrence(DateTime.Now); } await Task.Delay(5000, token); //5 seconds delay } while (!token.IsCancellationRequested); } }
How does it work
The first example for every minute (see Schedule property) processing for some imports:
/// <summary> /// Hosted service runs by timer ImportProcessing /// </summary> public class ImportProcessingHostedService : CrontabScheduledBackgroundHostedService { /// <inheritdoc /> public ImportProcessingHostedService( IServiceScopeFactory serviceScopeFactory, ILogger<ImportProcessingHostedService> logger) : base(serviceScopeFactory, logger) { } /// <inheritdoc /> protected override async Task ProcessInScope(IServiceProvider serviceProvider, CancellationToken token) { using (var scope = serviceProvider.CreateScope()) { var filePath = $"{Environment.CurrentDirectory}/{AppData.ApplicationFolders.Import}"; if (!Directory.Exists(filePath)) { Logger.LogInformation($"{ServiceName}: {filePath} not found. Creating folder [{filePath}]"); Directory.CreateDirectory(filePath); } var reservationImportService = scope.ServiceProvider.GetService<IReservationService>(); await reservationImportService.FindFileNextFileFromFolderAsync(); } } /// <inheritdoc /> protected override string Schedule => "*/1 * * * *"; /// <inheritdoc /> protected override string DisplayName => ServiceName; /// <inheritdoc /> protected override bool IsExecuteOnServerRestart => false; }
Another example demonstrates every night (at 0:03 AM) start for some processes:
/// <summary> /// Starts every night /// </summary> public class EveryNightHostedService : CrontabScheduledBackgroundHostedService { /// <inheritdoc /> public EveryNightHostedService( IServiceScopeFactory serviceScopeFactory, ILogger<EveryNightHostedService> logger) : base(serviceScopeFactory, logger) { } protected override Task ProcessInScope(IServiceProvider serviceProvider, CancellationToken token) { var workerService = serviceProvider.GetService<IWorkerService>(); return workerService.AppendWorkToUpdateCompetitorPricesAsync(); } protected override string Schedule => "3 0 * * *"; /// <inheritdoc /> protected override string DisplayName => ServiceName; /// <inheritdoc /> protected override bool IsExecuteOnServerRestart => false; }
And this is works very well. What do you think about this implementation? Please, leave a comment or send me feedback by feedback form.