.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);
我们正在使用 .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);