.NET Core中HostingEnvironment.QueueBackgroundWorkItem的替代解决方案

时间:2016-04-29 18:31:38

标签: asp.net-core asp.net-core-mvc background-process dnx .net-core

我们正在使用.NET Core Web Api,并寻找一种轻量级解决方案,将具有可变强度的请求记录到数据库中,但不希望客户端等待保存过程。
遗憾的是,HostingEnvironment.QueueBackgroundWorkItem(..)中没有dnx实施,Task.Run(..)并不安全。
有没有优雅的解决方案?

7 个答案:

答案 0 :(得分:10)

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

这样的HttpContext应该注册为单身,并且可以由任何其他组件通过DI使用。我目前正在使用它发送邮件,它工作正常(在应用程序关闭期间测试邮件发送)。

注意:访问后台任务中当前UnsafeQueueUserWorkItem之类的内容不应该有效。无论如何,old solution使用fa fa-arrow-right来禁止此行为。

您怎么看?

<强>更新

使用ASP.NET Core 2.0,后台任务有了新功能,使用ASP.NET Core 2.1会更好:Implementing background tasks in .NET Core 2.x webapps or microservices with IHostedService and the BackgroundService class

答案 1 :(得分:10)

正如@axelheer所说,IHostedService是.NET Core 2.0及更高版本的发展方向。

我需要像HostingEnvironment.QueueBackgroundWorkItem的ASP.NET核心替代版一样轻量级,所以我写了DalSoft.Hosting.BackgroundQueue,它使用了.NET Core的2.0 IHostedService

PM&GT;安装包DalSoft.Hosting.BackgroundQueue

在ASP.NET Core Startup.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();
}

答案 2 :(得分:8)

您可以将Hangfire(http://hangfire.io/)用于.NET Core中的后台作业。

例如:

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

答案 3 :(得分:5)

以下是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);
            }
        }
    }
}

答案 4 :(得分:0)

原始的HostingEnvironment.QueueBackgroundWorkItem是单线的,非常易于使用。 在ASP Core 2.x中执行此操作的“新”方法要求阅读神秘文档的页面并编写大量代码。

为避免这种情况,您可以使用以下替代方法

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

静态ConcurrentBag<Boolean> bs将保存对对象的引用,这将防止垃圾回收器在控制器返回后收集任务。

答案 5 :(得分:0)

我知道这有点晚了,但是我们也遇到了这个问题。因此,在阅读了很多想法之后,这就是我们想出的解决方案。

    /// <summary>
    /// Defines a simple interface for scheduling background tasks. Useful for UnitTesting ASP.net code
    /// </summary>
    public interface ITaskScheduler
    {
        /// <summary>
        /// Schedules a task which can run in the background, independent of any request.
        /// </summary>
        /// <param name="workItem">A unit of execution.</param>
        [SecurityPermission(SecurityAction.LinkDemand, Unrestricted = true)]
        void QueueBackgroundWorkItem(Action<CancellationToken> workItem);

        /// <summary>
        /// Schedules a task which can run in the background, independent of any request.
        /// </summary>
        /// <param name="workItem">A unit of execution.</param>
        [SecurityPermission(SecurityAction.LinkDemand, Unrestricted = true)]
        void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);
    }


    public class BackgroundTaskScheduler : BackgroundService, ITaskScheduler
    {
        public BackgroundTaskScheduler(ILogger<BackgroundTaskScheduler> logger)
        {
            _logger = logger;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            _logger.LogTrace("BackgroundTaskScheduler Service started.");

            _stoppingToken = stoppingToken;

            _isRunning = true;
            try
            {
                await Task.Delay(-1, stoppingToken);
            }
            catch (TaskCanceledException)
            {
            }
            finally
            {
                _isRunning = false;
                _logger.LogTrace("BackgroundTaskScheduler Service stopped.");
            }
        }

        public void QueueBackgroundWorkItem(Action<CancellationToken> workItem)
        {
            if (workItem == null)
            {
                throw new ArgumentNullException(nameof(workItem));
            }

            if (!_isRunning)
                throw new Exception("BackgroundTaskScheduler is not running.");

            _ = Task.Run(() => workItem(_stoppingToken), _stoppingToken);
        }

        public void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem)
        {
            if (workItem == null)
            {
                throw new ArgumentNullException(nameof(workItem));
            }

            if (!_isRunning)
                throw new Exception("BackgroundTaskScheduler is not running.");

            _ = Task.Run(async () =>
                {
                    try
                    {
                        await workItem(_stoppingToken);
                    }
                    catch (Exception e)
                    {
                        _logger.LogError(e, "When executing background task.");
                        throw;
                    }
                }, _stoppingToken);
        }

        private readonly ILogger _logger;
        private volatile bool _isRunning;
        private CancellationToken _stoppingToken;
    }

ITaskScheduler(我们已经在旧的ASP.NET客户端代码中为进行UTest测试而定义)允许客户端添加后台任务。 BackgroundTaskScheduler的主要目的是捕获停止取消令牌(由主机拥有)并将其传递到所有后台Task中;根据定义,该文件可以在System.Threading.ThreadPool中运行,因此无需创建我们自己的文件。

要正确配置托管服务,请参见this post

享受!

答案 6 :(得分:0)

我使用 Quartz.NET(不需要 SQL Server)和以下扩展方法来轻松设置和运行作业:

public class MyJobAsync :IJob
{
   public async Task Execute(IJobExecutionContext context)
   {
          var data = (MyDataType)context.MergedJobDataMap["data"];
          ....

数据作为必须可序列化的对象传递。创建一个处理作业的 IJob,如下所示:

await SchedulerInstance.CreateSingleJob<MyJobAsync>("JobTitle 123", myData);

像这样执行:

Date_Create