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