Loupe

Comment implémenter une background Task executée périodiquement avec ASP.NET Core.

ASP.NET Core nous fournit tous les outils pour pouvoir très facilement executer tous types de background task directement hébergées dans notre WebApp, notamment grâce à l'interface IHostedService disponible dans le namespace Microsoft.Extensions.Hosting.

L'interface définit 3 méthodes :

  • StartAsync()
  • StopAsync()
  • ExecuteAsync()

Ce sera à nous d'appeler ExecuteAsync() mais les méthodes StartAsync() et StopAsync() seront executées respectivement au démarrage et à l'arrêt de notre application. Plus spécifiquement durant les évènements. IApplicationLifetime.ApplicationStarted et IApplicationLifetime.ApplicationStopped.

Attention toutefois, il faut noter que lors d'une erreur ou un crash, StopAsync() peut ne pas être appelée. Il est donc judicieux de bien penser à dispose toutes nos ressources dans la méthode Dispose() qui elle est exécutée quoi qu'il arrive.

La 1ère étape consiste à créer/utiliser une classe qui implémente les méthodes StartAsync() et StopAsync() définies dans IHostedService généralement nommé BackgroundService.

Depuis la version 2.1, cette classe est disponible nativement dans Microsoft.Extensions.Hosting, pour les versions antérieures il suffit de copier la classe suivante :

  /// <summary>
    /// Base class for implementing a long running <see cref="IHostedService"/>.
    /// </summary>
    public abstract class BackgroundService : IHostedService, IDisposable
    {
        private Task _executingTask;
        private readonly CancellationTokenSource _stoppingCts = new CancellationTokenSource();

        /// <summary>
        /// This method is called when the <see cref="IHostedService"/> starts. The implementation should return a task that represents
        /// the lifetime of the long running operation(s) being performed.
        /// </summary>
        /// <param name="stoppingToken">Triggered when <see cref="IHostedService.StopAsync(CancellationToken)"/> is called.</param>
        /// <returns>A <see cref="Task"/> that represents the long running operations.</returns>
        protected abstract Task ExecuteAsync(CancellationToken stoppingToken);

        /// <summary>
        /// Triggered when the application host is ready to start the service.
        /// </summary>
        /// <param name="cancellationToken">Indicates that the start process has been aborted.</param>
        public virtual Task StartAsync(CancellationToken cancellationToken)
        {
            // Store the task we're executing
            _executingTask = ExecuteAsync(_stoppingCts.Token);

            // If the task is completed then return it, this will bubble cancellation and failure to the caller
            if (_executingTask.IsCompleted)
            {
                return _executingTask;
            }

            // Otherwise it's running
            return Task.CompletedTask;
        }

        /// <summary>
        /// Triggered when the application host is performing a graceful shutdown.
        /// </summary>
        /// <param name="cancellationToken">Indicates that the shutdown process should no longer be graceful.</param>
        public virtual async Task StopAsync(CancellationToken cancellationToken)
        {
            // Stop called without start
            if (_executingTask == null)
            {
                return;
            }

            try
            {
                // Signal cancellation to the executing method
                _stoppingCts.Cancel();
            }
            finally
            {
                // Wait until the task completes or the stop token triggers
                await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, cancellationToken));
            }

        }

        public virtual void Dispose()
        {
            _stoppingCts.Cancel();
        }
    }

Ensuite nous avons besoin d'une classe qui hérite de BackgroundService et implémente la methode ExecuteAsync() ainsi que le code pour exécuter notre logique périodiquement :

    public class PeriodicBackgroundService : BackgroundService
    {
        private CancellationTokenSource _cts;

        public PeriodicBackgroundService()
        {
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                try
                {
                    await ExecuteWork();

                    stoppingToken.ThrowIfCancellationRequested();

                    if (_cts == null || _cts.Token.IsCancellationRequested)
                    {
                        _cts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
                    }

                    await Task.Delay(TimeSpan.FromMinutes(5), _cts.Token);
                    stoppingToken.ThrowIfCancellationRequested();

                }
                catch (OperationCanceledException)
                {
                    // Log errors
                }
            }
        }

        private async Task ExecuteWork()
        {
            await Task.Delay(10000);
        }
    }

Et voilà ! Grace au Task.Delay() notre methode ExecuteWork() sera appelée toutes les 5 minutes tant que l'on ne stoppe pas la tâche.

Il ne nous reste plus qu'à déclarer notre service dans la methode ConfigureServices() de  Startup.cs.

services.AddSingleton<IHostedService, PeriodicBackgroundService>();

Ou depuis la version 2.1, plus simplement :

services.AddHostedService<PeriodicBackgroundService>();

Si on lance notre application nous avons maintenant une background task qui s’exécute toute les 5 minutes du démarrage à l’arrêt de notre site.

Pour les WebApp hébergées sur Azure, pensez que par défaut Azure éteint les sites s'ils sont inutilisés. L'option Always On existe pour garder notre site actif et se trouve dans Settings -> Application settings.

AlwaysOn.png

Nous avons donc vu comment en très peu de code nous pouvons exécuter des background task de façon periodique, mais ce n’est que l’une des implémentations possibles de IHostedService, avec les 3 méthodes définies dans l’interface, on peut très facilement imaginer d’autres systèmes comme faire une background task qui dépile et exécute une liste de tâches par exemple (peut-être même de façon périodique ?).

Laissez parler votre imagination.

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus