私たちは.NET Core Web Apiと連携しており、さまざまな強度のリクエストをデータベースに記録する軽量なソリューションを探していますが、クライアントが保存プロセスを待つことを望んでいません。
残念ながら、dnx
にはHostingEnvironment.QueueBackgroundWorkItem(..)
が実装されておらず、Task.Run(..)
は安全ではありません。
エレガントなソリューションはありますか?
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
のようなものにアクセスすることはできません。 古いソリューション は、UnsafeQueueUserWorkItem
を使用して、とにかくそれを禁止します。
どう思いますか?
更新:
ASP.NET Core 2.0には、バックグラウンドタスク用の新しいものがあり、ASP.NET Core 2.1ではより良くなります。 IHostedServiceおよびBackgroundServiceクラスを使用した.NET Core 2.x webappsまたはマイクロサービスでのバックグラウンドタスクの実装
@axelheerが述べたように、 IHostedService は.NET Core 2.0以降で使用する方法です。
HostingEnvironment.QueueBackgroundWorkItemの代わりにASP.NET Coreのような軽量のものが必要だったため、 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();
}
.NET Coreのバックグラウンドジョブには、Hangfire( http://hangfire.io/ )を使用できます。
例えば :
var jobId = BackgroundJob.Enqueue(
() => Console.WriteLine("Fire-and-forget!"));
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);
}
}
}
}
オリジナル 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
は、オブジェクトへの参照を保持します。これにより、コントローラーが戻った後にガベージコレクターがタスクを収集できなくなります。