ASP.NET Core 2.2: The standalone service for scheduling jobs

en-US | создано: 08.12.2019 | опубликовано: 08.12.2019 | обновлено: 18.12.2019 | просмотров за всё время: 506

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.

Нет комментариев