简体   繁体   English

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

[英]Alternative solution to HostingEnvironment.QueueBackgroundWorkItem in .NET Core

We are working with .NET Core Web Api, and looking for a lightweight solution to log requests with variable intensity into database, but don't want client's to wait for the saving process.我们正在使用 .NET Core Web Api,并寻找一种轻量级的解决方案来将可变强度的请求记录到数据库中,但不希望客户端等待保存过程。
Unfortunately there's no HostingEnvironment.QueueBackgroundWorkItem(..) implemented in dnx , and Task.Run(..) is not safe.不幸的是,在dnx没有实现HostingEnvironment.QueueBackgroundWorkItem(..) ,并且Task.Run(..)不安全。
Is there any elegant solution?有什么优雅的解决方案吗?

As @axelheer mentioned IHostedService is the way to go in .NET Core 2.0 and above.正如@axelheer 提到的, IHostedService是 .NET Core 2.0 及更高版本的方法。

I needed a lightweight like for like ASP.NET Core replacement for HostingEnvironment.QueueBackgroundWorkItem, so I wrote DalSoft.Hosting.BackgroundQueue which uses.NET Core's 2.0 IHostedService .我需要一个轻量级的像 ASP.NET Core 替换 HostingEnvironment.QueueBackgroundWorkItem,所以我写了DalSoft.Hosting.BackgroundQueue它使用 .NET Core 的 2.0 IHostedService

PM> Install-Package DalSoft.Hosting.BackgroundQueue PM> 安装包 DalSoft.Hosting.BackgroundQueue

In your ASP.NET Core Startup.cs:在 ASP.NET Core Startup.cs 中:

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

To queue a background Task just add BackgroundQueue to your controller's constructor and call Enqueue .要将后台任务排队,只需将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();
}

QueueBackgroundWorkItem is gone, but we've got IApplicationLifetime instead of IRegisteredObject , which is being used by the former one. QueueBackgroundWorkItem不见了,但我们得到了IApplicationLifetime而不是IRegisteredObject ,后者正在被前一个使用。 And it looks quite promising for such scenarios, I think.我认为,对于这种情况,它看起来很有希望。

The idea (and I'm still not quite sure, if it's a pretty bad one; thus, beware!) is to register a singleton, which spawns and observes new tasks.这个想法(我仍然不太确定,如果它很糟糕;因此,要小心!)是注册一个单例,它产生观察新任务。 Within that singleton we can furthermore register a "stopped event" in order to proper await still running tasks.在该单例中,我们还可以注册一个“停止事件”,以便正确等待仍在运行的任务。

This "concept" could be used for short running stuff like logging, mail sending, and the like.这个“概念”可用于短期运行的东西,如日志记录、邮件发送等。 Things, that should not take much time, but would produce unnecessary delays for the current request.事情,这不应该花费太多时间,但会为当前请求产生不必要的延迟。

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

Such a BackgroundPool should be registered as a singleton and can be used by any other component via DI.这样的BackgroundPool应该注册为单例,并且可以通过 DI 被任何其他组件使用。 I'm currently using it for sending mails and it works fine (tested mail sending during app shutdown too).我目前正在使用它来发送邮件并且工作正常(在应用程序关闭期间也测试了邮件发送)。

Note: accessing stuff like the current HttpContext within the background task should not work.注意:在后台任务中访问像当前HttpContext这样的东西应该不起作用。 The old solution uses UnsafeQueueUserWorkItem to prohibit that anyway. 旧的解决方案无论如何都使用UnsafeQueueUserWorkItem来禁止它。

What do you think?你怎么看?

Update:更新:

With ASP.NET Core 2.0 there's new stuff for background tasks, which get's better with ASP.NET Core 2.1: Implementing background tasks in .NET Core 2.x webapps or microservices with IHostedService and the BackgroundService class使用 ASP.NET Core 2.0 有后台任务的新内容,使用 ASP.NET Core 2.1 变得更好: 在 .NET Core 2.x webapps 或微服务中使用 IHostedService 和 BackgroundService 类实现后台任务

You can use Hangfire ( http://hangfire.io/ ) for background jobs in .NET Core.您可以将 Hangfire ( http://hangfire.io/ ) 用于 .NET Core 中的后台作业。

For example :例如:

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

Here is a tweaked version of Axel's answer that lets you pass in delegates and does more aggressive cleanup of completed tasks.这是Axel 答案的调整版本,可让您传入委托并对已完成的任务进行更积极的清理。

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

I have used Quartz.NET (does not require SQL Server) with the following extension method to easily set up and run a job:我使用Quartz.NET (不需要 SQL Server)和以下扩展方法来轻松设置和运行作业:

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

Data is passed as an object that must be serializable.数据作为必须可序列化的对象传递。 Create an IJob that processes the job like this:创建一个处理作业的 IJob,如下所示:

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

Execute like this:像这样执行:

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

I know this is a little late, but we just ran into this issue too.我知道这有点晚了,但我们也遇到了这个问题。 So after reading lots of ideas, here's the solution we came up with.所以在阅读了很多想法之后,这是我们想出的解决方案。

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

The ITaskScheduler (which we already defined in our old ASP.NET client code for UTest test purposes) allows a client to add a background task. ITaskScheduler (我们已经在旧的 ASP.NET 客户端代码中定义用于 UTest 测试目的)允许客户端添加后台任务。 The main purpose of the BackgroundTaskScheduler is to capture the stop cancellation token (which is own by the Host) and to pass it into all the background Task s; BackgroundTaskScheduler的主要目的是捕获停止取消令牌(由 Host 拥有)并将其传递到所有后台Task which by definition, runs in the System.Threading.ThreadPool so there is no need to create our own.根据定义,它在System.Threading.ThreadPool运行,因此无需创建我们自己的。

To configure Hosted Services properly see this post .要正确配置托管服务,请参阅此帖子

Enjoy!享受!

The original HostingEnvironment.QueueBackgroundWorkItem was a one-liner and very convenient to use.最初的HostingEnvironment.QueueBackgroundWorkItem是单行的,使用起来非常方便。 The "new" way of doing this in ASP Core 2.x requires reading pages of cryptic documentation and writing considerable amount of code.在 ASP Core 2.x 中执行此操作的“新”方法需要阅读神秘文档页面并编写大量代码。

To avoid this you can use the following alternative method为避免这种情况,您可以使用以下替代方法

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

The static ConcurrentBag<Boolean> bs will hold a reference to the object, this will prevent garbage collector from collecting the task after the controller returns.静态ConcurrentBag<Boolean> bs将持有对对象的引用,这将防止垃圾收集器在控制器返回后收集任务。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 HostingEnvironment.QueueBackgroundWorkItem - 澄清? - HostingEnvironment.QueueBackgroundWorkItem - Clarification? ASP.NET中的HostingEnvironment.QueueBackgroundWorkItem()用于小型后台任务 - HostingEnvironment.QueueBackgroundWorkItem() in ASP.NET for small background tasks HostingEnvironment.QueueBackgroundWorkItem真的会延迟回收吗? - Does HostingEnvironment.QueueBackgroundWorkItem really delay recycling? HostingEnvironment.QueueBackgroundWorkItem支持多个后台进程 - HostingEnvironment.QueueBackgroundWorkItem support for multiple background processs HostingEnvironment.QueueBackgroundWorkItem中的事务范围超时 - Transaction scope in HostingEnvironment.QueueBackgroundWorkItem time out WCF 中带有 HostingEnvironment.QueueBackgroundWorkItem 的后台线程 - Background thread with HostingEnvironment.QueueBackgroundWorkItem in WCF HostingEnvironment.QueueBackgroundWorkItem和HostingEnvironment.RegisterObject之间的区别 - Difference between HostingEnvironment.QueueBackgroundWorkItem and HostingEnvironment.RegisterObject HostingEnvironment.QueueBackgroundWorkItem 永远不会在 azure 部署的 asp.net mvc 4.5 应用程序上执行 - HostingEnvironment.QueueBackgroundWorkItem never executes on azure deployed asp.net mvc 4.5 application 在一定时间后强制取消HostingEnvironment.QueueBackgroundWorkItem - Forcing HostingEnvironment.QueueBackgroundWorkItem to cancel after a certain time asp.net core 2.0中System.Web.Hosting.HostingEnvironment.RegisterObject的替代方案 - alternative for System.Web.Hosting.HostingEnvironment.RegisterObject in asp.net core 2.0
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM