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

作成者: Jeow Li Huan

注意

これは、この記事の最新バージョンではありません。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

重要

この情報はリリース前の製品に関する事項であり、正式版がリリースされるまでに大幅に変更される可能性があります。 Microsoft はここに示されている情報について、明示か黙示かを問わず、一切保証しません。

現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

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

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

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

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

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

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

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

Package

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

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

IHostedService インターフェイス

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

StartAsync

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

StartAsync は、実行時間の短いタスクに制限する必要があります。これは、ホストされたサービスが順番に実行され、StartAsync の実行が完了するまで、それ以上のサービスは開始されないためです。

StopAsync

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

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

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

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

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

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

BackgroundService 基底クラス

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

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

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

詳細については、BackgroundService のソース コードを参照してください。

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

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

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

    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.cs で開始されます。

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

非同期の時間指定バックグラウンド タスク

次のコードは、非同期の時間指定バックグラウンド タスクを作成します。

namespace TimedBackgroundTasks;

public class TimedHostedService : BackgroundService
{
    private readonly ILogger<TimedHostedService> _logger;
    private int _executionCount;

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

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

        // When the timer should have no due-time, then do the work once now.
        DoWork();

        using PeriodicTimer timer = new(TimeSpan.FromSeconds(1));

        try
        {
            while (await timer.WaitForNextTickAsync(stoppingToken))
            {
                DoWork();
            }
        }
        catch (OperationCanceledException)
        {
            _logger.LogInformation("Timed Hosted Service is stopping.");
        }
    }

    // Could also be a async method, that can be awaited in ExecuteAsync above
    private void DoWork()
    {
        int count = Interlocked.Increment(ref _executionCount);

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

ネイティブ AOT

Worker Service テンプレートは、--aot フラグを使用した .NET ネイティブの Ahead-of-Time (AOT) をサポートします。

  1. 新しいプロジェクトを作成します。
  2. [ワーカー サービス] を選択します。 [次へ] を選択します。
  3. [プロジェクト名] フィールドにプロジェクト名を入力するか、既定のプロジェクト名をそのまま使用します。 [次へ] を選択します。
  4. [追加情報] ダイアログで、次を行います。
  5. [フレームワーク] を選択します。
  6. [Enable native AOT publish] (ネイティブ AOT 発行を有効にする) チェックボックスをオンにします。
  7. [作成] を選択します

AOT オプションにより、<PublishAot>true</PublishAot> がプロジェクト ファイルに追加されます。


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

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <InvariantGlobalization>true</InvariantGlobalization>
+   <PublishAot>true</PublishAot>
    <UserSecretsId>dotnet-WorkerWithAot-e94b2</UserSecretsId>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0-preview.4.23259.5" />
  </ItemGroup>
</Project>

その他の技術情報

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

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

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

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

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

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

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

Package

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

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

IHostedService インターフェイス

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

StartAsync

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

StartAsync は、実行時間の短いタスクに制限する必要があります。これは、ホストされたサービスが順番に実行され、StartAsync の実行が完了するまで、それ以上のサービスは開始されないためです。

StopAsync

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

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

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

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

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

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

BackgroundService 基底クラス

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

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

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

詳細については、BackgroundService のソース コードを参照してください。

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

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

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

    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.cs で開始されます。

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

非同期の時間指定バックグラウンド タスク

次のコードは、非同期の時間指定バックグラウンド タスクを作成します。

namespace TimedBackgroundTasks;

public class TimedHostedService : BackgroundService
{
    private readonly ILogger<TimedHostedService> _logger;
    private int _executionCount;

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

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

        // When the timer should have no due-time, then do the work once now.
        DoWork();

        using PeriodicTimer timer = new(TimeSpan.FromSeconds(1));

        try
        {
            while (await timer.WaitForNextTickAsync(stoppingToken))
            {
                DoWork();
            }
        }
        catch (OperationCanceledException)
        {
            _logger.LogInformation("Timed Hosted Service is stopping.");
        }
    }

    // Could also be a async method, that can be awaited in ExecuteAsync above
    private void DoWork()
    {
        int count = Interlocked.Increment(ref _executionCount);

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

その他の技術情報

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

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

キャンセル トークンには、シャットダウン プロセスが正常に行われないことを示す、既定の 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();

その他の技術情報