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