Мы работаем с .NET Core Web Api и ищем легкое решение для запросов журнала с переменной интенсивностью в базу данных, но не хотим, чтобы клиент дождался процесса сохранения.
К сожалению, нет HostingEnvironment.QueueBackgroundWorkItem(..)
, реализованного в dnx
, а Task.Run(..)
небезопасно.
Есть ли элегантное решение?
Альтернативное решение для HostingEnvironment.QueueBackgroundWorkItem в .NET Core
Ответ 1
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
как текущий HttpContext
в фоновой задаче, не должен работать. Старое решение использует UnsafeQueueUserWorkItem
чтобы в любом случае запретить это.
Как вы думаете?
Обновить:
В ASP.NET Core 2.0 появился новый материал для фоновых задач, который улучшается в ASP.NET Core 2.1: Реализация фоновых задач в веб-приложениях или микросервисах .NET Core 2.x с помощью IHostedService и класса BackgroundService.
Ответ 2
Как упомянул @axelheer, IHostedService - это путь в .NET Core 2.0 и выше.
Мне нужна была легкая замена для ASP.NET Core для HostingEnvironment.QueueBackgroundWorkItem, поэтому я написал DalSoft.Hosting.BackgroundQueue, который использует .NET Core 2.0 IHostedService.
PM> Инсталляционный пакет 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();
}
Ответ 3
Вы можете использовать Hangfire (http://hangfire.io/) для фоновых заданий в .NET Core.
Например:
var jobId = BackgroundJob.Enqueue(
() => Console.WriteLine("Fire-and-forget!"));
Ответ 4
Ниже приведена измененная версия Axel 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);
}
}
}
}
Ответ 5
Оригинальный 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
будет содержать ссылку на объект, что не позволит сборщику мусора собирать задачу после возврата контроллера.