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

Alternative solution to HostingEnvironment.QueueBackgroundWorkItem in .NET Core

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

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

例如:

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

QueueBackgroundWorkItem 不见了,但是我们得到了 IApplicationLifetime 而不是前一个正在使用的 IRegisteredObject。我认为这种情况看起来很有希望。

这个想法(我仍然不太确定,如果它是一个非常糟糕的想法;因此,请注意!)是注册一个单身人士,它产生 观察新任务。在该单例中,我们还可以注册一个 "stopped event" 以便正确等待 运行 任务。

此 "concept" 可用于简短的 运行 内容,例如日志记录、邮件发送等。事情,不应该花费太多时间,但会对当前请求产生不必要的延迟。

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

这是 的调整版本,可让您传递委托并对已完成的任务进行更积极的清理。

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

正如@axelheer 提到的那样,IHostedService 是进入 .NET Core 2.0 及更高版本的方式。

我需要一个轻量级的 ASP.NET 核心替换 HostingEnvironment.QueueBackgroundWorkItem,所以我写了 DalSoft.Hosting.BackgroundQueue uses.NET核心 2.0 IHostedService.

PM> 安装包 DalSoft.Hosting.BackgroundQueue

在你的 ASP.NET 核心 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();
}

原来的HostingEnvironment.QueueBackgroundWorkItem是one-liner,用起来很方便。 在 ASP 核心 2.x 中执行此操作的 "new" 方法需要阅读神秘文档页面并编写大量代码。

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

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

static ConcurrentBag<Boolean> bs 将持有对象的引用,这将防止垃圾收集器在控制器 returns 之后收集任务。

我知道这有点晚了,但我们也 运行 进入了这个问题。所以在阅读了很多想法之后,这是我们想出的解决方案。

    /// <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的主要目的是捕获停止取消令牌(Host拥有)并将其传递给所有后台Task;根据定义,它在 System.Threading.ThreadPool 中运行,因此无需创建我们自己的。

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

尽情享受吧!

我已经使用 Quartz.NET(不需要 SQL 服务器)和以下扩展方法来轻松设置和 运行 工作:

public static class QuartzUtils
{
        public static async Task<JobKey> CreateSingleJob<JOB>(this IScheduler scheduler,
            string jobName, object data) where JOB : IJob
        {
            var jm = new JobDataMap { { "data", data } };

            var jobKey = new JobKey(jobName);

            await scheduler.ScheduleJob(
                JobBuilder.Create<JOB>()
                .WithIdentity(jobKey)
                .Build(),

                TriggerBuilder.Create()
                .WithIdentity(jobName)
                .UsingJobData(jm)
                .StartNow()
                .Build());

            return jobKey;
        }
}

数据作为必须可序列化的对象传递。创建一个像这样处理作业的 IJob:

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

这样执行:

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