ASP.NET Core でホステッド サービスを使用するバックグラウンド タスク

作成者: Jeow Li Huan

ASP.NET Core では、バックグラウンド タスクを ホステッド サービス として実装することができます。 ホストされるサービスは、IHostedService インターフェイスを実装するバックグラウンド タスク ロジックを持つクラスです。 このトピックでは、3 つのホステッド サービスの例について説明します。

  • タイマーで実行されるバックグラウンド タスク。
  • スコープ サービスをアクティブ化するホステッド サービス。 スコープ サービスは依存関係の挿入 (DI) を使用できます。
  • 連続して実行される、キューに格納されたバックグラウンド タスク。

サンプル コードを表示またはダウンロードします (ダウンロード方法)。

ワーカー サービス テンプレート

ASP.NET Core ワーカー サービス テンプレートは、実行時間が長いサービス アプリを作成する場合の出発点として利用できます。 ワーカー サービス テンプレートから作成されたアプリで、そのプロジェクト ファイル内のワーカー SDK が指定されます。

<Project Sdk="Microsoft.NET.Sdk.Worker">

ホステッド サービス アプリの基礎としてテンプレートを使用するには:

  1. 新しいプロジェクトを作成します。
  2. [ワーカー サービス] を選択します。 [次へ] を選択します。
  3. [プロジェクト名] フィールドにプロジェクト名を入力するか、既定のプロジェクト名をそのまま使用します。 [作成] を選択します。
  4. [新しい Worker サービスを作成します] ダイアログで、[作成] を選択します。

Package

ワーカー サービス テンプレートに基づくアプリは Microsoft.NET.Sdk.Worker SDK を使用し、Microsoft.Extensions.Hosting パッケージへの明示的なパッケージ参照を含んでいます。 たとえば、サンプル アプリのプロジェクト ファイル (BackgroundTasksSample.csproj) を参照してください。

Microsoft.NET.Sdk.Web SDK を使用する Web アプリの場合、Microsoft.Extensions.Hosting パッケージは共有フレームワークから暗黙的に参照されます。 アプリのプロジェクト ファイル内の明示的なパッケージ参照は必要ありません。

IHostedService インターフェイス

IHostedService インターフェイスは、ホストによって管理されるオブジェクトの 2 つのメソッドを定義します。

  • StartAsync(CancellationToken): StartAsync には、バックグラウンド タスクを開始するロジックが含まれています。 StartAsync は、以下よりも "前に" 呼び出されます。

    既定の動作を変更して、アプリのパイプラインが構成されて ApplicationStarted が呼び出された後で、ホステッド サービスの StartAsync が実行するようにできます。 既定の動作を変更するには、ConfigureWebHostDefaults を呼び出した後でホステッド サービス (次の例では VideosWatcher) を追加します。

    using Microsoft.AspNetCore.Hosting;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;
    
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }
    
        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                })
                .ConfigureServices(services =>
                {
                    services.AddHostedService<VideosWatcher>();
                });
    }
    
  • StopAsync(CancellationToken):ホストが正常なシャットダウンを実行しているときにトリガーされます。 StopAsync には、バックグラウンド タスクを終了するロジックが含まれています。 アンマネージ リソースを破棄するには、IDisposableファイナライザー (デストラクター) を実装します。

    キャンセル トークンには、シャットダウン プロセスが正常に行われないことを示す、既定の 5 秒間のタイムアウトが含まれています。 キャンセルがトークンに要求された場合:

    • アプリで実行されている残りのバックグラウンド操作が中止します。
    • StopAsync で呼び出されたすべてのメソッドが速やかに戻ります。

    ただし、キャンセルが要求された後もタスクは破棄されません—呼び出し元がすべてのタスクの完了を待機します。

    アプリが予期せずシャットダウンした場合 (たとえば、アプリのプロセスが失敗した場合)、StopAsync は呼び出されないことがあります。 そのため、StopAsync で呼び出されたメソッドや行われた操作が実行されない可能性があります。

    既定の 5 秒のシャットダウン タイムアウトを延長するには、次を設定します。

ホステッド サービスは、アプリの起動時に一度アクティブ化され、アプリのシャットダウン時に正常にシャットダウンされます。 バックグラウンド タスクの実行中にエラーがスローされた場合、StopAsync が呼び出されていなくても Dispose を呼び出す必要があります。

BackgroundService 基底クラス

BackgroundService は、長期 IHostedService を実装するための基底クラスです。

ExecuteAsync(CancellationToken) は、バックグラウンド サービスを実行するために呼び出されます。 この実装では、バックグラウンド サービスの有効期間全体を表す Task が返されます。 await を呼び出すなどして ExecuteAsync が非同期になるまで、以降のサービスは開始されません。 ExecuteAsync では、長時間の初期化作業を実行しないようにしてください。 ホストは StopAsync(CancellationToken) でブロックされ、ExecuteAsync の完了を待機します。

IHostedService.StopAsync が呼び出されると、キャンセル トークンがトリガーされます。 サービスを正常にシャットダウンするためのキャンセル トークンが起動すると、ExecuteAsync の実装はすぐに終了します。 それ以外の場合は、シャットダウンのタイムアウト時にサービスが強制的にシャットダウンします。 詳細については、「IHostedService インターフェイス」のセクションを参照してください。

StartAsync は、実行時間の短いタスクに制限する必要があります。これは、ホストされたサービスが順番に実行され、StartAsync の実行が完了するまで、それ以上のサービスは開始されないためです。 実行時間の長いタスクは、ExecuteAsync に配置する必要があります。 詳細については、BackgroundServiceのソースを参照してください。

時間が指定されたバックグラウンド タスク

時間が指定されたバックグラウンド タスクは、System.Threading.Timer クラスを利用します。 このタイマーはタスクの DoWork メソッドをトリガーします。 タイマーは StopAsync で無効になり、Dispose でサービス コンテナーが破棄されたときに破棄されます。

public class TimedHostedService : IHostedService, IDisposable
{
    private int executionCount = 0;
    private readonly ILogger<TimedHostedService> _logger;
    private Timer _timer;

    public TimedHostedService(ILogger<TimedHostedService> logger)
    {
        _logger = logger;
    }

    public Task StartAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Hosted Service running.");

        _timer = new Timer(DoWork, null, TimeSpan.Zero, 
            TimeSpan.FromSeconds(5));

        return Task.CompletedTask;
    }

    private void DoWork(object state)
    {
        var count = Interlocked.Increment(ref executionCount);

        _logger.LogInformation(
            "Timed Hosted Service is working. Count: {Count}", count);
    }

    public Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Timed Hosted Service is stopping.");

        _timer?.Change(Timeout.Infinite, 0);

        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _timer?.Dispose();
    }
}

前の DoWork の実行が完了するまで Timer は待機されないため、ここで示したアプローチはすべてのシナリオに適しているとは限りません。 Interlocked.Increment は、アトミック操作として実行カウンターをインクリメントするために使用されされます。これにより、複数のスレッドによって executionCount が同時に更新されなくなります。

サービスは、AddHostedService 拡張メソッドを使用して IHostBuilder.ConfigureServices (Program.cs) に登録されます。

services.AddHostedService<TimedHostedService>();

バックグラウンド タスクでスコープ サービスを使用する

BackgroundService 内でスコープ サービスを使用するには、スコープを作成します。 既定では、ホステッド サービスのスコープは作成されません。

バックグラウンド タスクのスコープ サービスには、バックグラウンド タスクのロジックが含まれています。 次に例を示します。

  • サービスは非同期です。 DoWork メソッドは Task を返します。 デモンストレーションのために、10 秒の遅延が DoWork メソッドで待機されます。
  • ILogger がサービスに挿入されます。
internal interface IScopedProcessingService
{
    Task DoWork(CancellationToken stoppingToken);
}

internal class ScopedProcessingService : IScopedProcessingService
{
    private int executionCount = 0;
    private readonly ILogger _logger;
    
    public ScopedProcessingService(ILogger<ScopedProcessingService> logger)
    {
        _logger = logger;
    }

    public async Task DoWork(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            executionCount++;

            _logger.LogInformation(
                "Scoped Processing Service is working. Count: {Count}", executionCount);

            await Task.Delay(10000, stoppingToken);
        }
    }
}

ホステッド サービスはスコープを作成してバックグラウンド タスクのスコープ サービスを解決し、DoWork メソッドを呼び出します。 ExecuteAsync で待機していた DoWorkTask を返します。

public class ConsumeScopedServiceHostedService : BackgroundService
{
    private readonly ILogger<ConsumeScopedServiceHostedService> _logger;

    public ConsumeScopedServiceHostedService(IServiceProvider services, 
        ILogger<ConsumeScopedServiceHostedService> logger)
    {
        Services = services;
        _logger = logger;
    }

    public IServiceProvider Services { get; }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service running.");

        await DoWork(stoppingToken);
    }

    private async Task DoWork(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is working.");

        using (var scope = Services.CreateScope())
        {
            var scopedProcessingService = 
                scope.ServiceProvider
                    .GetRequiredService<IScopedProcessingService>();

            await scopedProcessingService.DoWork(stoppingToken);
        }
    }

    public override async Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is stopping.");

        await base.StopAsync(stoppingToken);
    }
}

サービスは IHostBuilder.ConfigureServices (Program.cs) に登録されています。 ホステッド サービスは、AddHostedService 拡張メソッドを使用して登録されます。

services.AddHostedService<ConsumeScopedServiceHostedService>();
services.AddScoped<IScopedProcessingService, ScopedProcessingService>();

キューに格納されたバックグラウンド タスク

バックグラウンド タスク キューは、.NET 4.x QueueBackgroundWorkItem に基づいています。

public interface IBackgroundTaskQueue
{
    ValueTask QueueBackgroundWorkItemAsync(Func<CancellationToken, ValueTask> workItem);

    ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
        CancellationToken cancellationToken);
}

public class BackgroundTaskQueue : IBackgroundTaskQueue
{
    private readonly Channel<Func<CancellationToken, ValueTask>> _queue;

    public BackgroundTaskQueue(int capacity)
    {
        // Capacity should be set based on the expected application load and
        // number of concurrent threads accessing the queue.            
        // BoundedChannelFullMode.Wait will cause calls to WriteAsync() to return a task,
        // which completes only when space became available. This leads to backpressure,
        // in case too many publishers/calls start accumulating.
        var options = new BoundedChannelOptions(capacity)
        {
            FullMode = BoundedChannelFullMode.Wait
        };
        _queue = Channel.CreateBounded<Func<CancellationToken, ValueTask>>(options);
    }

    public async ValueTask QueueBackgroundWorkItemAsync(
        Func<CancellationToken, ValueTask> workItem)
    {
        if (workItem == null)
        {
            throw new ArgumentNullException(nameof(workItem));
        }

        await _queue.Writer.WriteAsync(workItem);
    }

    public async ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(
        CancellationToken cancellationToken)
    {
        var workItem = await _queue.Reader.ReadAsync(cancellationToken);

        return workItem;
    }
}

次の QueueHostedService の例では以下のようになります。

  • ExecuteAsync で待機していた BackgroundProcessing メソッドが Taskを返します。
  • BackgroundProcessing で、キュー内のバックグラウンド タスクがデキューされ、実行されます。
  • 作業項目が待機されてから、StopAsync でサービスが停止します。
public class QueuedHostedService : BackgroundService
{
    private readonly ILogger<QueuedHostedService> _logger;

    public QueuedHostedService(IBackgroundTaskQueue taskQueue, 
        ILogger<QueuedHostedService> logger)
    {
        TaskQueue = taskQueue;
        _logger = logger;
    }

    public IBackgroundTaskQueue TaskQueue { get; }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(
            $"Queued Hosted Service is running.{Environment.NewLine}" +
            $"{Environment.NewLine}Tap W to add a work item to the " +
            $"background queue.{Environment.NewLine}");

        await BackgroundProcessing(stoppingToken);
    }

    private async Task BackgroundProcessing(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var workItem = 
                await TaskQueue.DequeueAsync(stoppingToken);

            try
            {
                await workItem(stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, 
                    "Error occurred executing {WorkItem}.", nameof(workItem));
            }
        }
    }

    public override async Task StopAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Queued Hosted Service is stopping.");

        await base.StopAsync(stoppingToken);
    }
}

MonitorLoop サービスは、w キーが入力デバイスで選択されると常に、ホステッド サービスのためにタスクのエンキューを処理します。

  • IBackgroundTaskQueueMonitorLoop サービスに挿入されます。
  • IBackgroundTaskQueue.QueueBackgroundWorkItem が呼び出され、作業項目がエンキューされます。
  • 作業項目により、実行時間の長いバックグラウンド タスクがシミュレートされます。
    • 5 秒間の遅延が 3 回実行されます (Task.Delay)。
    • タスクが取り消された場合、try-catch ステートメントによって OperationCanceledException がトラップされます。
public class MonitorLoop
{
    private readonly IBackgroundTaskQueue _taskQueue;
    private readonly ILogger _logger;
    private readonly CancellationToken _cancellationToken;

    public MonitorLoop(IBackgroundTaskQueue taskQueue, 
        ILogger<MonitorLoop> logger, 
        IHostApplicationLifetime applicationLifetime)
    {
        _taskQueue = taskQueue;
        _logger = logger;
        _cancellationToken = applicationLifetime.ApplicationStopping;
    }

    public void StartMonitorLoop()
    {
        _logger.LogInformation("MonitorAsync Loop is starting.");

        // Run a console user input loop in a background thread
        Task.Run(async () => await MonitorAsync());
    }

    private async ValueTask MonitorAsync()
    {
        while (!_cancellationToken.IsCancellationRequested)
        {
            var keyStroke = Console.ReadKey();

            if (keyStroke.Key == ConsoleKey.W)
            {
                // Enqueue a background work item
                await _taskQueue.QueueBackgroundWorkItemAsync(BuildWorkItem);
            }
        }
    }

    private async ValueTask BuildWorkItem(CancellationToken token)
    {
        // Simulate three 5-second tasks to complete
        // for each enqueued work item

        int delayLoop = 0;
        var guid = Guid.NewGuid().ToString();

        _logger.LogInformation("Queued Background Task {Guid} is starting.", guid);

        while (!token.IsCancellationRequested && delayLoop < 3)
        {
            try
            {
                await Task.Delay(TimeSpan.FromSeconds(5), token);
            }
            catch (OperationCanceledException)
            {
                // Prevent throwing if the Delay is cancelled
            }

            delayLoop++;

            _logger.LogInformation("Queued Background Task {Guid} is running. " + "{DelayLoop}/3", guid, delayLoop);
        }

        if (delayLoop == 3)
        {
            _logger.LogInformation("Queued Background Task {Guid} is complete.", guid);
        }
        else
        {
            _logger.LogInformation("Queued Background Task {Guid} was cancelled.", guid);
        }
    }
}

サービスは IHostBuilder.ConfigureServices (Program.cs) に登録されています。 ホステッド サービスは、AddHostedService 拡張メソッドを使用して登録されます。

services.AddSingleton<MonitorLoop>();
services.AddHostedService<QueuedHostedService>();
services.AddSingleton<IBackgroundTaskQueue>(ctx => {
    if (!int.TryParse(hostContext.Configuration["QueueCapacity"], out var queueCapacity))
        queueCapacity = 100;
    return new BackgroundTaskQueue(queueCapacity);
});

MonitorLoopProgram.Main で開始されます。

var monitorLoop = host.Services.GetRequiredService<MonitorLoop>();
monitorLoop.StartMonitorLoop();

ASP.NET Core では、バックグラウンド タスクを ホステッド サービス として実装することができます。 ホストされるサービスは、IHostedService インターフェイスを実装するバックグラウンド タスク ロジックを持つクラスです。 このトピックでは、3 つのホステッド サービスの例について説明します。

  • タイマーで実行されるバックグラウンド タスク。
  • スコープ サービスをアクティブ化するホステッド サービス。 スコープ サービスは依存関係の挿入 (DI) を使用できます
  • 連続して実行される、キューに格納されたバックグラウンド タスク。

サンプル コードを表示またはダウンロードします (ダウンロード方法)。

Package

Microsoft.AspNetCore.App メタパッケージを参照するか、Microsoft.Extensions.Hosting パッケージへのパッケージ参照を追加します。

IHostedService インターフェイス

ホステッド サービスでは、IHostedService インターフェイスを実装します。 このインターフェイスは、ホストによって管理されるオブジェクトの 2 つのメソッドを定義します。

  • StartAsync(CancellationToken): StartAsync には、バックグラウンド タスクを開始するロジックが含まれています。 Web ホスト を使用する場合は、サーバーが起動し、IApplicationLifetime.ApplicationStarted がトリガーされた後で、StartAsync が呼び出されます。 汎用ホスト を使用する場合は、ApplicationStarted がトリガーされる前に StartAsync が呼び出されます。

  • StopAsync(CancellationToken):ホストが正常なシャットダウンを実行しているときにトリガーされます。 StopAsync には、バックグラウンド タスクを終了するロジックが含まれています。 アンマネージ リソースを破棄するには、IDisposableファイナライザー (デストラクター) を実装します。

    キャンセル トークンには、シャットダウン プロセスが正常に行われないことを示す、既定の 5 秒間のタイムアウトが含まれています。 キャンセルがトークンに要求された場合:

    • アプリで実行されている残りのバックグラウンド操作が中止します。
    • StopAsync で呼び出されたすべてのメソッドが速やかに戻ります。

    ただし、キャンセルが要求された後もタスクは破棄されません—呼び出し元がすべてのタスクの完了を待機します。

    アプリが予期せずシャットダウンした場合 (たとえば、アプリのプロセスが失敗した場合)、StopAsync は呼び出されないことがあります。 そのため、StopAsync で呼び出されたメソッドや行われた操作が実行されない可能性があります。

    既定の 5 秒のシャットダウン タイムアウトを延長するには、次を設定します。

ホステッド サービスは、アプリの起動時に一度アクティブ化され、アプリのシャットダウン時に正常にシャットダウンされます。 バックグラウンド タスクの実行中にエラーがスローされた場合、StopAsync が呼び出されていなくても Dispose を呼び出す必要があります。

時間が指定されたバックグラウンド タスク

時間が指定されたバックグラウンド タスクは、System.Threading.Timer クラスを利用します。 このタイマーはタスクの DoWork メソッドをトリガーします。 タイマーは StopAsync で無効になり、Dispose でサービス コンテナーが破棄されたときに破棄されます。

internal class TimedHostedService : IHostedService, IDisposable
{
    private readonly ILogger _logger;
    private Timer _timer;

    public TimedHostedService(ILogger<TimedHostedService> logger)
    {
        _logger = logger;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Timed Background Service is starting.");

        _timer = new Timer(DoWork, null, TimeSpan.Zero, 
            TimeSpan.FromSeconds(5));

        return Task.CompletedTask;
    }

    private void DoWork(object state)
    {
        _logger.LogInformation("Timed Background Service is working.");
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Timed Background Service is stopping.");

        _timer?.Change(Timeout.Infinite, 0);

        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _timer?.Dispose();
    }
}

前の DoWork の実行が完了するまで Timer は待機されないため、ここで示したアプローチはすべてのシナリオに適しているとは限りません。

サービスは、AddHostedService 拡張メソッドを使用して Startup.ConfigureServices に登録されます。

services.AddHostedService<TimedHostedService>();

バックグラウンド タスクでスコープ サービスを使用する

IHostedService 内でスコープ サービスを使用するには、スコープを作成します。 既定では、ホステッド サービスのスコープは作成されません。

バックグラウンド タスクのスコープ サービスには、バックグラウンド タスクのロジックが含まれています。 次の例では、ILogger がサービスに挿入されています。

internal interface IScopedProcessingService
{
    void DoWork();
}

internal class ScopedProcessingService : IScopedProcessingService
{
    private readonly ILogger _logger;
    
    public ScopedProcessingService(ILogger<ScopedProcessingService> logger)
    {
        _logger = logger;
    }

    public void DoWork()
    {
        _logger.LogInformation("Scoped Processing Service is working.");
    }
}

ホステッド サービスはスコープを作成してバックグラウンド タスクのスコープ サービスを解決し、DoWork メソッドを呼び出します。

internal class ConsumeScopedServiceHostedService : IHostedService
{
    private readonly ILogger _logger;

    public ConsumeScopedServiceHostedService(IServiceProvider services, 
        ILogger<ConsumeScopedServiceHostedService> logger)
    {
        Services = services;
        _logger = logger;
    }

    public IServiceProvider Services { get; }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is starting.");

        DoWork();

        return Task.CompletedTask;
    }

    private void DoWork()
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is working.");

        using (var scope = Services.CreateScope())
        {
            var scopedProcessingService = 
                scope.ServiceProvider
                    .GetRequiredService<IScopedProcessingService>();

            scopedProcessingService.DoWork();
        }
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is stopping.");

        return Task.CompletedTask;
    }
}

サービスは Startup.ConfigureServices に登録されています。 IHostedService の実装は、AddHostedService 拡張メソッドで登録されます。

services.AddHostedService<ConsumeScopedServiceHostedService>();
services.AddScoped<IScopedProcessingService, ScopedProcessingService>();

キューに格納されたバックグラウンド タスク

バックグラウンド タスク キューは、.NET Framework 4.x QueueBackgroundWorkItem (暫定的に ASP.NET Core に組み込まれる予定) に基づいています。

public interface IBackgroundTaskQueue
{
    void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);

    Task<Func<CancellationToken, Task>> DequeueAsync(
        CancellationToken cancellationToken);
}

public class BackgroundTaskQueue : IBackgroundTaskQueue
{
    private ConcurrentQueue<Func<CancellationToken, Task>> _workItems = 
        new ConcurrentQueue<Func<CancellationToken, Task>>();
    private SemaphoreSlim _signal = new SemaphoreSlim(0);

    public void QueueBackgroundWorkItem(
        Func<CancellationToken, Task> workItem)
    {
        if (workItem == null)
        {
            throw new ArgumentNullException(nameof(workItem));
        }

        _workItems.Enqueue(workItem);
        _signal.Release();
    }

    public async Task<Func<CancellationToken, Task>> DequeueAsync(
        CancellationToken cancellationToken)
    {
        await _signal.WaitAsync(cancellationToken);
        _workItems.TryDequeue(out var workItem);

        return workItem;
    }
}

QueueHostedService では、キュー内のバックグラウンド タスクは BackgroundService としてデキューされ、実行されます。これは、長時間実行する IHostedService を構成するための基本クラスです。

public class QueuedHostedService : BackgroundService
{
    private readonly ILogger _logger;

    public QueuedHostedService(IBackgroundTaskQueue taskQueue, 
        ILoggerFactory loggerFactory)
    {
        TaskQueue = taskQueue;
        _logger = loggerFactory.CreateLogger<QueuedHostedService>();
    }

    public IBackgroundTaskQueue TaskQueue { get; }

    protected async override Task ExecuteAsync(
        CancellationToken cancellationToken)
    {
        _logger.LogInformation("Queued Hosted Service is starting.");

        while (!cancellationToken.IsCancellationRequested)
        {
            var workItem = await TaskQueue.DequeueAsync(cancellationToken);

            try
            {
                await workItem(cancellationToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, 
                   "Error occurred executing {WorkItem}.", nameof(workItem));
            }
        }

        _logger.LogInformation("Queued Hosted Service is stopping.");
    }
}

サービスは Startup.ConfigureServices に登録されています。 IHostedService の実装は、AddHostedService 拡張メソッドで登録されます。

services.AddHostedService<QueuedHostedService>();
services.AddSingleton<IBackgroundTaskQueue, BackgroundTaskQueue>();

インデックス ページ モデル クラスで:

  • IBackgroundTaskQueue がコンストラクターに挿入され、Queue に割り当てられます。
  • IServiceScopeFactory が挿入され、_serviceScopeFactory に割り当てられます。 ファクトリは、スコープ内でサービス作成するための IServiceScope のインスタンス作成に使用されます。 スコープは、アプリの AppDbContext (スコープ サービス) を使用し、データベース レコードを IBackgroundTaskQueue (シングルトン サービス) に書き込むために作成されます。
public class IndexModel : PageModel
{
    private readonly AppDbContext _db;
    private readonly ILogger _logger;
    private readonly IServiceScopeFactory _serviceScopeFactory;

    public IndexModel(AppDbContext db, IBackgroundTaskQueue queue, 
        ILogger<IndexModel> logger, IServiceScopeFactory serviceScopeFactory)
    {
        _db = db;
        _logger = logger;
        Queue = queue;
        _serviceScopeFactory = serviceScopeFactory;
    }

    public IBackgroundTaskQueue Queue { get; }

[インデックス] ページで [タスクの追加] ボタンを選択すると、OnPostAddTask メソッドが実行されます。 QueueBackgroundWorkItem が呼び出され、作業項目がエンキューされます。

public IActionResult OnPostAddTaskAsync()
{
    Queue.QueueBackgroundWorkItem(async token =>
    {
        var guid = Guid.NewGuid().ToString();

        using (var scope = _serviceScopeFactory.CreateScope())
        {
            var scopedServices = scope.ServiceProvider;
            var db = scopedServices.GetRequiredService<AppDbContext>();

            for (int delayLoop = 1; delayLoop < 4; delayLoop++)
            {
                try
                {
                    db.Messages.Add(
                        new Message() 
                        { 
                            Text = $"Queued Background Task {guid} has " +
                                $"written a step. {delayLoop}/3"
                        });
                    await db.SaveChangesAsync();
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, 
                        "An error occurred writing to the " +
                        "database. Error: {Message}", ex.Message);
                }

                await Task.Delay(TimeSpan.FromSeconds(5), token);
            }
        }

        _logger.LogInformation(
            "Queued Background Task {Guid} is complete. 3/3", guid);
    });

    return RedirectToPage();
}

その他の技術情報