web-dev-qa-db-fra.com

Solution alternative à HostingEnvironment.QueueBackgroundWorkItem dans .NET Core

Nous travaillons avec .NET Core Web Api et recherchons une solution légère pour enregistrer les demandes d'intensité variable dans la base de données, mais nous ne voulons pas que le client attende le processus d'enregistrement.
Malheureusement, il n'y a pas de HostingEnvironment.QueueBackgroundWorkItem(..) implémentée dans dnx, et Task.Run(..) n'est pas sûre.
Existe-t-il une solution élégante?

38

QueueBackgroundWorkItem a disparu, mais nous avons IApplicationLifetime au lieu de IRegisteredObject, qui est utilisé par l'ancien. Et cela semble assez prometteur pour de tels scénarios, je pense.

L'idée (et je ne suis toujours pas tout à fait sûr, si elle est assez mauvaise; donc, méfiez-vous!) Est d'enregistrer un singleton, qui engendre et observe de nouvelles tâches. Dans ce singleton, nous pouvons en outre enregistrer un "événement arrêté" afin d'attendre correctement les tâches en cours d'exécution.

Ce "concept" pourrait être utilisé pour des tâches de courte durée comme la journalisation, l'envoi de courrier, etc. Les choses, qui ne devraient pas prendre beaucoup de temps, mais qui entraîneraient des retards inutiles pour la demande actuelle.

public class BackgroundPool
{
    protected ILogger<BackgroundPool> Logger { get; }

    public BackgroundPool(ILogger<BackgroundPool> logger, IApplicationLifetime lifetime)
    {
        if (logger == null)
            throw new ArgumentNullException(nameof(logger));
        if (lifetime == null)
            throw new ArgumentNullException(nameof(lifetime));

        lifetime.ApplicationStopped.Register(() =>
        {
            lock (currentTasksLock)
            {
                Task.WaitAll(currentTasks.ToArray());
            }

            logger.LogInformation(BackgroundEvents.Close, "Background pool closed.");
        });

        Logger = logger;
    }

    private readonly object currentTasksLock = new object();

    private readonly List<Task> currentTasks = new List<Task>();

    public void SendStuff(Stuff whatever)
    {
        var task = Task.Run(async () =>
        {
            Logger.LogInformation(BackgroundEvents.Send, "Sending stuff...");

            try
            {
                // do THE stuff

                Logger.LogInformation(BackgroundEvents.SendDone, "Send stuff returns.");
            }
            catch (Exception ex)
            {
                Logger.LogError(BackgroundEvents.SendFail, ex, "Send stuff failed.");
            }
        });

        lock (currentTasksLock)
        {
            currentTasks.Add(task);

            currentTasks.RemoveAll(t => t.IsCompleted);
        }
    }
}

Un tel BackgroundPool doit être enregistré en tant que singleton et peut être utilisé par tout autre composant via DI. Je l'utilise actuellement pour envoyer des e-mails et cela fonctionne très bien (envoi de courrier testé également lors de l'arrêt de l'application).

Remarque: l'accès à des choses comme le HttpContext actuel dans la tâche en arrière-plan ne devrait pas fonctionner. ancienne solution utilise UnsafeQueueUserWorkItem pour interdire cela de toute façon.

Qu'est-ce que tu penses?

Mise à jour:

Avec ASP.NET Core 2.0, il y a de nouvelles choses pour les tâches d'arrière-plan, qui s'améliorent avec ASP.NET Core 2.1: Implémentation de tâches d'arrière-plan dans les webapps ou les microservices .NET Core 2.x avec IHostedService et la classe BackgroundService

11
Axel Heer

Comme @axelheer l'a mentionné IHostedService est la voie à suivre dans .NET Core 2.0 et supérieur.

J'avais besoin d'un léger comme pour le remplacement ASP.NET Core pour HostingEnvironment.QueueBackgroundWorkItem, j'ai donc écrit DalSoft.Hosting.BackgroundQueue qui utilise .NET Core 2.0 IHostedService .

PM> Install-Package DalSoft.Hosting.BackgroundQueue

Dans votre Startup.cs ASP.NET Core:

public void ConfigureServices(IServiceCollection services)
{
   services.AddBackgroundQueue(onException:exception =>
   {

   });
}

Pour mettre en file d'attente une tâche en arrière-plan, ajoutez simplement BackgroundQueue au constructeur de votre contrôleur et appelez Enqueue.

public EmailController(BackgroundQueue backgroundQueue)
{
   _backgroundQueue = backgroundQueue;
}

[HttpPost, Route("/")]
public IActionResult SendEmail([FromBody]emailRequest)
{
   _backgroundQueue.Enqueue(async cancellationToken =>
   {
      await _smtp.SendMailAsync(emailRequest.From, emailRequest.To, request.Body);
   });

   return Ok();
}
11
DalSoft

Vous pouvez utiliser Hangfire ( http://hangfire.io/ ) pour les tâches d'arrière-plan dans .NET Core.

Par exemple :

var jobId = BackgroundJob.Enqueue(
    () => Console.WriteLine("Fire-and-forget!"));
8
ycrumeyrolle

Voici une version modifiée de réponse d'Axel qui vous permet de passer des délégués et de nettoyer plus agressivement les tâches terminées.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging;

namespace Example
{
    public class BackgroundPool
    {
        private readonly ILogger<BackgroundPool> _logger;
        private readonly IApplicationLifetime _lifetime;
        private readonly object _currentTasksLock = new object();
        private readonly List<Task> _currentTasks = new List<Task>();

        public BackgroundPool(ILogger<BackgroundPool> logger, IApplicationLifetime lifetime)
        {
            if (logger == null)
                throw new ArgumentNullException(nameof(logger));
            if (lifetime == null)
                throw new ArgumentNullException(nameof(lifetime));

            _logger = logger;
            _lifetime = lifetime;

            _lifetime.ApplicationStopped.Register(() =>
            {
                lock (_currentTasksLock)
                {
                    Task.WaitAll(_currentTasks.ToArray());
                }

                _logger.LogInformation("Background pool closed.");
            });
        }

        public void QueueBackgroundWork(Action action)
        {
#pragma warning disable 1998
            async Task Wrapper() => action();
#pragma warning restore 1998

            QueueBackgroundWork(Wrapper);
        }

        public void QueueBackgroundWork(Func<Task> func)
        {
            var task = Task.Run(async () =>
            {
                _logger.LogTrace("Queuing background work.");

                try
                {
                    await func();

                    _logger.LogTrace("Background work returns.");
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex.HResult, ex, "Background work failed.");
                }
            }, _lifetime.ApplicationStopped);

            lock (_currentTasksLock)
            {
                _currentTasks.Add(task);
            }

            task.ContinueWith(CleanupOnComplete, _lifetime.ApplicationStopping);
        }

        private void CleanupOnComplete(Task oldTask)
        {
            lock (_currentTasksLock)
            {
                _currentTasks.Remove(oldTask);
            }
        }
    }
}
5

L'original HostingEnvironment.QueueBackgroundWorkItem Était monoligne et très pratique à utiliser. La "nouvelle" façon de procéder dans ASP Core 2.x nécessite la lecture de pages de documentation cryptée et l'écriture d'une quantité considérable de code.

Pour éviter cela, vous pouvez utiliser la méthode alternative suivante

    public static ConcurrentBag<Boolean> bs = new ConcurrentBag<Boolean>();

    [HttpPost("/save")]
    public async Task<IActionResult> SaveAsync(dynamic postData)
    {

    var id = (String)postData.id;

    Task.Run(() =>
                {
                    bs.Add(Create(id));
                });

     return new OkResult();

    }


    private Boolean Create(String id)
    {
      /// do work
      return true;
    }

Le statique ConcurrentBag<Boolean> bs Contiendra une référence à l'objet, cela empêchera le garbage collector de collecter la tâche après le retour du contrôleur.

0
user11658885