Мы работаем с .NET Core Web Api и ищете легкое решение для запросов журнала с переменной интенсивностью в базу данных, но не хотим, чтобы клиент ожидал процесса сохранения ,
К сожалению, нет HostingEnvironment.QueueBackgroundWorkItem(..)
, реализованный в dnx
, а Task.Run(..)
небезопасен.
Есть ли элегантное решение?Альтернативное решение для HostingEnvironment.QueueBackgroundWorkItem в .NET Core
ответ
Вы можете использовать Hangfire (http://hangfire.io/) для фоновых заданий в .NET Core.
Например:
var jobId = BackgroundJob.Enqueue(
() => Console.WriteLine("Fire-and-forget!"));
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
должно быть зарегистрировано в качестве одноэлементных и может быть использован любой другой компонент через DI. В настоящее время я использую его для отправки писем, и он отлично работает (проверенная отправка почты также при отключении приложения).
Примечание: доступ к материалам, таким как текущий HttpContext
в фоновом режиме, не должен работать. old solution использует UnsafeQueueUserWorkItem
, чтобы все равно запретить.
Как вы думаете?
Update:
С ASP.NET ядро 2.0 есть новый материал для фоновых задач, которые получают лучше с ASP.NET 2.1 Ядра: Implementing background tasks in .NET Core 2.x webapps or microservices with IHostedService and the BackgroundService class
Вот приспособленная версия 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);
}
}
}
}
Как и в ответе Акселя, вы фактически не ожидаете возвращения задачи из «Task.WaitAll (currentTask.ToArray());». – Shazi
Как упоминалось @axelheer IHostedService это путь в .NET 2.0 Ядро и выше.
Мне нужен был легкий вес, как для замены ASP.NET Core для HostingEnvironment.QueueBackgroundWorkItem, поэтому я написал DalSoft.Hosting.BackgroundQueue, который использует .NET Core 2.0 IHostedService.
PM> Install-Package DalSoft.Hosting.BackgroundQueue
В вашей сердцевинной запуске ASP.NET.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();
}
Почему голос? Похож на очень хороший вопрос. QueueBackgroundWorkItem, безусловно, очень полезен. –
'HostingEnvironment.QueueBackgroundWorkItem' тоже небезопасен. Это было менее опасно, чем 'Task.Run', но это было небезопасно. –
Хороший вопрос. Я сам пытаюсь внедрить репортер прогрессивных сигналов (используя интерфейс IProgress), но из-за асинхронного характера SignalR мне нужно обрабатывать отчет о ходе работы как задачи (хотя и очень короткие задачи), не замедляя работу, о которой они сообщают , – Shazi