24

我們正在使用.NET Core Web Api,並尋找一種輕量級解決方案來將具有可變強度的請求記錄到數據庫中,但不希望客戶端等待保存過程。
不幸的是沒有HostingEnvironment.QueueBackgroundWorkItem(..)dnxTask.Run(..)實施是不安全的。
有沒有優雅的解決方案?.NET中的HostingEnvironment.QueueBackgroundWorkItem的替代解決方案核心

+5

爲什麼選擇投票?對我來說這是一個非常好的問題。 QueueBackgroundWorkItem肯定非常有用。 –

+2

'HostingEnvironment.QueueBackgroundWorkItem'也不安全。它比'Task.Run'更不安全,但它不安全。 –

+0

一個很好的問題。我自己試圖實現一個signalR級別的記者(使用IProgress接口),但由於SignalR的異步性質,我需要將進度報告作爲任務來處理(儘管是非常短暫的任務),而不會減慢他們報告的操作。 – Shazi

回答

7

您可以在.NET Core中使用Hangfire(http://hangfire.io/)作爲後臺作業。

例如:

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

QueueBackgroundWorkItem走了,但我們已經有了IApplicationLifetime代替IRegisteredObject,正在使用由前一個。我想這對於這樣的場景看起來很有希望。這個想法(我還不太確定,如果是非常糟糕的;因此,小心!)是註冊一個單身人士,其產生觀察新的任務。在那個單例中,我們還可以註冊一個「停止的事件」,以便正確地等待仍在運行的任務。

這個「概念」可以用於短時間運行的東西,如日誌記錄,郵件發送等。事情不應該花費太多時間,但會對當前請求產生不必要的延遲。

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

這種BackgroundPool應註冊爲獨立的,並可以通過經由DI的任何其它部件一起使用。我目前正在使用它發送郵件,並且它工作正常(在應用程序關閉期間也測試了郵件發送)。

注意:訪問像後臺任務中的當前HttpContext這樣的東西應該不起作用。無論如何,old solution使用UnsafeQueueUserWorkItem來禁止。

您認爲如何?

更新:

在ASP.NET 2.0的核心有後臺任務,從而獲得更好的ASP.NET 2.1核心的新東西:Implementing background tasks in .NET Core 2.x webapps or microservices with IHostedService and the BackgroundService class

+0

在您的ApplicationStopped.Register委託中,您實際上並不等待從「Task.WaitAll(currentTask.ToArray());」返回的任務。使這種呼籲有點毫無意義。 – Shazi

+0

WaitAll已經在等待。也許你的意思是WhenAll? –

+0

是的,你是對的,我的錯。 – Shazi

4

這裏是一個Axel's answer版本扭捏,讓你傳入代表並對已完成的任務進行更積極的清理。

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

就像在Axel的回答中一樣,您實際上並不等待從「Task.WaitAll(currentTask.ToArray());」返回的任務。 – Shazi

3

如@axelheer提到IHostedService是在.NET核心2.0及以上的路要走。

我需要一個類似於ASP.NET Core的替代HostingEnvironment.QueueBackgroundWorkItem的輕量級,所以我寫了使用.NET Core的2.0 IHostedServiceDalSoft.Hosting.BackgroundQueue

PM>安裝,包裝DalSoft.Hosting.BackgroundQueue

在你的ASP.NET核心啓動。CS:

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

    }); 
} 

要排隊一個後臺任務只需添加BackgroundQueue到控制器的構造函數,並調用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(); 
}