プログラムを中心とした個人的なメモ用のブログです。 タイトルは迷走中。
内容の保証はできませんのであしからずご了承ください。

2020/01/16

ASP.NET Core (.NET Core) でバックグランドタスクを起動する

event_note2020/01/16 2:53

ASP.NET Core (.NET Core) において、アプリケーションの起動に合わせてバックグラウンドタスクを起動させる方法です。

環境

  • Visual Studio 2017
  • .NET Core 2.2

概要

IHostedService を実装したサービスクラスを作成します。
IHostedService を使用するには、Nuget で Microsoft.Extensions.Hosting を追加する必要があります。

サービスクラスを IHostedService として DI コンテナに登録すると、Host が自動でバックグラウンドタスクとして実行してくれるようです。

Host については、作成するアプリケーションによって以下の2種類があるようです。

  • ASP.NET Core で使用する Web ホスト (IWebHostBuilder)
  • ASP.NET Core 以外で使用する汎用ホスト (HostBuilder)

ASP.NET Core の場合、テンプレートからプロジェクトを作成した段階で Web ホストが使用されている状態なので、特に意識はしなくても大丈夫そうですが、コンソールアプリなどの場合は汎用ホストを使用するように実装する必要があります。

ASP.NET Core の場合(Web ホスト)

ASP.NET Core のほうが話が簡単なので、先に ASP.NET Core で説明します。
コンソールアプリなどでは必要となる汎用ホストについては後述します。

IHostedService の実装

公式のドキュメントに詳しく書いてありますが、ほとんどのバックグラウンドタスクではタスクの開始や終了といった動作は同じであるため、IHostedService を実装した抽象クラスを作成しておくと良いようです。

サンプルコードをそのまま引用します。

// Copyright (c) .NET Foundation. Licensed under the Apache License, Version 2.0.
/// <summary>
/// Base class for implementing a long running <see cref="IHostedService"/>.
/// </summary>
public abstract class BackgroundService : IHostedService, IDisposable
{
    private Task _executingTask;
    private readonly CancellationTokenSource _stoppingCts =
                                                   new CancellationTokenSource();

    protected abstract Task ExecuteAsync(CancellationToken stoppingToken);

    public virtual Task StartAsync(CancellationToken cancellationToken)
    {
        // Store the task we're executing
        _executingTask = ExecuteAsync(_stoppingCts.Token);

        // If the task is completed then return it,
        // this will bubble cancellation and failure to the caller
        if (_executingTask.IsCompleted)
        {
            return _executingTask;
        }

        // Otherwise it's running
        return Task.CompletedTask;
    }

    public virtual async Task StopAsync(CancellationToken cancellationToken)
    {
        // Stop called without start
        if (_executingTask == null)
        {
            return;
        }

        try
        {
            // Signal cancellation to the executing method
            _stoppingCts.Cancel();
        }
        finally
        {
            // Wait until the task completes or the stop token triggers
            await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite,
                                                          cancellationToken));
        }

    }

    public virtual void Dispose()
    {
        _stoppingCts.Cancel();
    }
}

そして、この抽象クラスを継承してバックグランドで動かしたいサービスクラスを定義します。
コードの概要は以下です。

public class SampleService : BackgroundService
{
    public SampleService()
    {
        // コンストラクタへの引数は自動で DI される
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // ここに常駐処理を定義する
    }
}

ExecuteAsync 内では while で常駐処理を定義し、タスクの終了のチェックなども行います。
以下、公式のドキュメントのサンプルを引用します。

public class GracePeriodManagerService : BackgroundService
{
    private readonly ILogger<GracePeriodManagerService> _logger;
    private readonly OrderingBackgroundSettings _settings;

    private readonly IEventBus _eventBus;

    
    public GracePeriodManagerService(IOptions<OrderingBackgroundSettings> settings,
                                     IEventBus eventBus,
                                     ILogger<GracePeriodManagerService> logger)
    {
        //Constructor’s parameters validations...
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogDebug($"GracePeriodManagerService is starting.");

        stoppingToken.Register(() =>
            _logger.LogDebug($" GracePeriod background task is stopping."));

        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogDebug($"GracePeriod task doing background work.");

            // This eShopOnContainers method is querying a database table
            // and publishing events into the Event Bus (RabbitMQ / ServiceBus)
            CheckConfirmedGracePeriodOrders();

            await Task.Delay(_settings.CheckUpdateTime, stoppingToken);
        }

        _logger.LogDebug($"GracePeriod background task is stopping.");
    }

    .../...
}

DI コンテナへのサービスの追加

Startup.csConfigureServices メソッドで、サービスクラスを DI コンテナに登録します。

IHostedService を実装したクラスを AddHostedService で登録していきます。
AddSingleton でも可)

public void ConfigureServices(IServiceCollection services)
{
    // ...

    services.AddHostedService<MyHostedServiceA>();
    services.AddHostedService<MyHostedServiceB>();

    // または
        
    services.AddSingleton<IHostedService, MyHostedServiceA>();
    services.AddSingleton<IHostedService, MyHostedServiceB>();
}

汎用ホスト

ASP.NET Core 以外(例えばコンソールアプリなど)では汎用ホストを作成する必要があります。
基本的なコードは以下です。

public static async Task Main(string[] args)
{
    var host = new HostBuilder()
        .Build();

    await host.RunAsync();
}

IHostedService の実装

前述の ASP.NET Core と同様のため省略します。

DI コンテナへのサービス追加

IHostedService を DI するためにも、DI コンテナにサービスを追加する必要があります。

ConfigureServices で行います。

var host = new HostBuilder()
    .ConfigureServices((hostContext, services) =>
    {
        services.AddHostedService<MyHostedServiceA>();
        services.AddHostedService<MyHostedServiceB>();
        
        // または
        
        services.AddSingleton<IHostedService, MyHostedServiceA>();
        services.AddSingleton<IHostedService, MyHostedServiceB>();
    })
    .Build();

    await host.RunAsync();

ConfigureServices の中身については ASP.NET Core と同じです。

他にもいろいろ設定可能ですが、ここでは DI コンテナしか関係ないので、省略します。
詳細は以下を参照してください。