24

Wir arbeiten mit .NET Core-Web Api, und auf der Suche nach einer leichten Lösung Anfragen mit variabler Intensität in der Datenbank zu protokollieren, aber nicht wollen, die nicht Client für den Speichervorgang warten .
Leider ist HostingEnvironment.QueueBackgroundWorkItem(..) in dnx implementiert und Task.Run(..) ist nicht sicher.
Gibt es eine elegante Lösung?Alternative Lösung HostingEnvironment.QueueBackgroundWorkItem in .NET-Core

+5

Warum die Abstimmung unten? Sieht für mich sehr gut aus. QueueBackgroundWorkItem ist sicherlich sehr nützlich. –

+2

'HostingEnvironment.QueueBackgroundWorkItem' war auch nicht sicher. Es war weniger unsicher als "Task.Run", aber es war nicht sicher. –

+0

Eine gute Frage. Ich selbst versuche einen SignalR-Fortschrittsreporter zu implementieren (unter Verwendung der IProgress-Schnittstelle), aber aufgrund der asynchronen Natur von SignalR muss ich die Fortschrittsmeldung als Aufgabe behandeln (wenn auch sehr kurzlebige Aufgaben), ohne den Vorgang, über den sie berichten, zu verlangsamen . – Shazi

Antwort

7

Sie können Hangfire (http://hangfire.io/) für Hintergrundjobs in .NET Core verwenden.

Zum Beispiel:

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

QueueBackgroundWorkItem ist weg, aber wir haben IApplicationLifetime statt IRegisteredObject bekommen, die von den ersteren verwendet wird. Und für solche Szenarien sieht es vielversprechend aus, denke ich.

Die Idee (und ich bin mir immer noch nicht ganz sicher, wenn es ein ziemlich schlechter ist; also, Vorsicht!) Ist ein Singleton registrieren, der und neue Aufgaben beobachtet. Innerhalb dieses Singletons können wir außerdem ein "stopped event" registrieren, um noch laufende Aufgaben abzuwarten.

Dieses "Konzept" könnte für kurz laufende Dinge wie Protokollierung, Mail-Versand und dergleichen verwendet werden. Dinge, die nicht viel Zeit brauchen, aber für die aktuelle Anfrage unnötige Verzögerungen erzeugen würden.

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); 
     } 
    } 
} 

solches BackgroundPool sollte als ein Singleton registriert werden und kann durch jede andere Komponente über DI verwendet werden. Ich benutze es derzeit zum Senden von E-Mails und es funktioniert gut (getestete E-Mails senden auch während App-Shutdown).

Hinweis: Zugriff auf Sachen wie die aktuelle HttpContext innerhalb der Hintergrundaufgabe sollte nicht funktionieren. Die old solution verwendet UnsafeQueueUserWorkItem, um das sowieso zu verbieten.

Was denkst du?

Update:

Mit ASP.NET Core 2.0 gibt es neue Sachen für die Hintergrundaufgaben, die die besser mit ASP.NET-Core 2.1: Implementing background tasks in .NET Core 2.x webapps or microservices with IHostedService and the BackgroundService class

+0

In Ihrem ApplicationStopped.Register-Delegat warten Sie nicht wirklich auf die Aufgabe, die von "Task.WaitAll (currentTask.ToArray());" zurückgegeben wird. Diesen Anruf sinnlos zu machen. – Shazi

+0

WaitAll wartet bereits. Vielleicht meinst du WhenAll? –

+0

Ja, Sie haben Recht, mein Fehler. – Shazi

4

Hier ist eine gezwickt Version von Axel's answer, mit dem Sie Delegierte übergeben und die abgeschlossenen Aufgaben aggressiver säubern.

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); 
      } 
     } 
    } 
} 
+0

Genau wie in Axels Antwort warten Sie nicht wirklich auf die Aufgabe von "Task.WaitAll (currentTask.ToArray());". – Shazi

3

Wie @axelheer erwähnt IHostedService die Art und Weise ist in .NET Core 2.0 und höher zu gehen.

brauchte ich eine leichte wie für wie ASP.NET Core-Ersatz für HostingEnvironment.QueueBackgroundWorkItem, so schrieb ich DalSoft.Hosting.BackgroundQueue die uses.NET Core 2.0 IHostedService.

PM> Install-Package DalSoft.Hosting.BackgroundQueue

In ASP.NET-Core-Startup.cs:

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

    }); 
} 

Um eine Hintergrund-Task-Queue nur hinzufügen BackgroundQueue zu Ihrem Controller Konstruktor und rufen 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(); 
} 
Verwandte Themen