📘

ASP.NET Core などで使われている Generic Host (汎用ホスト) を見てみよう

2021/08/03に公開

最近 (といっても何年も前からですが…) ASP.NET Core などの Web アプリケーションを作ると以下のようなコードが Program.cs に書いてあったりします。

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace WebApplication5
{
    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>();
                });
    }
}

今回見てみるのは、この Host.CreateDefaultBuilder(args)Host クラスなどについてです。 ドキュメント的には以下に書いてあるような内容です。

https://docs.microsoft.com/ja-jp/dotnet/core/extensions/generic-host

試してみよう

この汎用ホストですが Web アプリケーション専用というわけではなくコンソール アプリケーションなどでも使うことが出来ます。ということで早速ハローワールドしてみましょう。

プロジェクトの作成と初期設定

適当にコンソール アプリケーションを作成します。今回は Visual Studio 2022 で .NET 6 (どちらも 2021/08/03 時点ではプレビュー版ですが .NET 5 でも大差ないと思います) で作成します。そして Microsoft.Extensions.Hosting パッケージを追加します。

まずは何もしないプログラムから作っていきます。Program.cs を以下のように書いて実行してみましょう。

using Microsoft.Extensions.Hosting;

Host.CreateDefaultBuilder(args)
    .Build()
    .Run();

実行すると以下のような結果になると思います。

info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: C:\Users\k_ota\source\repos\ConsoleApp12\ConsoleApp12\bin\Debug\net6.0

書いてある通りに Ctrl + C を押すと以下のようなログが出て終了します。

info: Microsoft.Hosting.Lifetime[0]
      Application is shutting down...

C:\Users\k_ota\source\repos\ConsoleApp12\ConsoleApp12\bin\Debug\net6.0\ConsoleApp12.exe (process 3236) exited with code 0.

ストンと落ちるのではなくいい感じですね。

ハローワールドを出してみよう

ということで土台が出来たのでハローワールドに取り掛かります。ドキュメントにもある通り IHostService インターフェースを実装したクラスを登録しておくと起動時に StartAsync メソッドが呼び出されます。ついでに終了時には StopAsync が呼ばれます。つまりハローワールドを出すなら StartAsync が一番お手軽そうですね。早速やってみましょう。

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;

Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        // IHostedService を実装したクラスを登録
        services.AddHostedService<HelloWorldHostedService>();
    })
    .Build()
    .Run();

// 起動時に Hello world を表示する HostedService
class HelloWorldHostedService : IHostedService
{
    public Task StartAsync(CancellationToken cancellationToken)
    {
        Console.WriteLine("Hello world");
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        // ついでに別れの挨拶も
        Console.WriteLine("Goodbye!");
        return Task.CompletedTask;
    }
}

さて、じゃぁ実行してみましょう。実行して Ctrl + C で終了させると以下のような結果になりました。

Hello world
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: C:\Users\k_ota\source\repos\ConsoleApp12\ConsoleApp12\bin\Debug\net6.0
info: Microsoft.Hosting.Lifetime[0]
      Application is shutting down...
Goodbye!

C:\Users\k_ota\source\repos\ConsoleApp12\ConsoleApp12\bin\Debug\net6.0\ConsoleApp12.exe (process 16224) exited with code 0.

バックグラウンド処理も実装しよう

IHostedService を実装したクラスの中に BackgroundService というクラスがいます。このクラスには ExecuteAsync というメソッドが実装されていて、このクラスを継承して ExecuteAsync を実装すると、その中で長時間実行可能なタスクを行うことが出来ます。
このクラスの StartAsyncStopAsyncExecuteAsync の呼び出しや終了待機なども行っていて、さらに Host の実装の中で BackgroundServiceExecuteAsync で例外が発生したときの挙動などをハンドリングしてくれます。

以下のようにすると Ctrl + C を押すまで延々と 1 秒間隔で こんにちは!! が表示されます。

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;

Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        // BackgroundService を実装したクラスを登録
        services.AddHostedService<HelloWorldHostedService>();
    })
    .Build()
    .Run();

// 起動時に Hello world を表示する HostedService
class HelloWorldHostedService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // 長期間実行されるタスク
        while (!stoppingToken.IsCancellationRequested)
        {
            Console.WriteLine("こんにちは!!");
            try
            {
                await Task.Delay(1000, stoppingToken);
            }
            catch (OperationCanceledException)
            { 
                break; 
            }
        }
    }
}

実行して数秒間放置して Ctrl + C を押すと以下のような感じになります。思った通りですね。

こんにちは!!
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: C:\Users\k_ota\source\repos\ConsoleApp12\ConsoleApp12\bin\Debug\net6.0
こんにちは!!
こんにちは!!
こんにちは!!
こんにちは!!
info: Microsoft.Hosting.Lifetime[0]
      Application is shutting down...

因みに BackgroundServiceExecuteAsync で例外が出るとアプリケーションも終了します。因みに処理されない例外という形でプロセスがクラッシュするのではなく、例外はきちんとハンドリングされた状態で終了処理が行われます。因みに BackgroundService が例外が発生したときの挙動はデフォルトだと例外処理をしてアプリケーションの終了なんですが、一応例外が発生しても動き続けるようにも出来ます。これはホスト単位の設定で ConfigureHostOptions 拡張メソッドで BackgroundServiceExceptionBehavior というプロパティの値を変えることで設定できます。

例えば以下のようにすると例外が起きてもアプリケーションは終了しません。

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;

Host.CreateDefaultBuilder(args)
    .ConfigureHostOptions(options =>
    {
        // 例外は無視
        options.BackgroundServiceExceptionBehavior = BackgroundServiceExceptionBehavior.Ignore;
    })
    .ConfigureServices(services =>
    {
        // IHostedService を実装したクラスを登録
        services.AddHostedService<HelloWorldHostedService>();
    })
    .Build()
    .Run();

class HelloWorldHostedService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // 絶対失敗するマン
        await Task.Delay(1000);
        throw new InvalidOperationException();
    }
}

当然例外で終わった BackgroundService は再起動したりはしないのですがアプリケーション自体は終了しません。まぁ使うことはあまりない設定だとは思いますが、こういったカスタマイズも出来るよってことで。

デフォルトの構成でおこなっていること

さて、最後に CreateDefaultBuilder でやってることだけをまとめて終わろうと思います。といってもこれは、ドキュメントの 既定の builder 設定 に書いてある内容のままです。

  • コンテンツルートは GetCurrentDirectory() の結果が設定される
  • 以下の場所からホストの構成を読み込む
    • DOTNET_ で始まる環境変数
    • コマンドライン引数
  • アプリの構成の読み込み
    • appsettings.json
    • appsettings.{Environment}.json
    • Development 環境の場合はユーザーシークレットからも読み込み
    • 環境変数
    • コマンドライン引数
  • ログプロバイダーの追加
    • コンソール
    • デバッグ
    • EventSource
    • イベントログ(Windows のみ)

まとめ

ということで軽く汎用ホストを見てみました。これをよく使うのは多分 ASP.NET Core だと思うのですが、今後実装されるフレームワーク系は、この汎用ホストをベースにして作られていくと思います。なので、ここの動きをおさえておくと色々潰しが効くと思います。

ここに書いたこと以外のことも公式ドキュメントには書いてあったりするので汎用ホストに興味が出た人は最初にも紹介したドキュメントも見てみてください。

一応再掲しておきます。

https://docs.microsoft.com/ja-jp/dotnet/core/extensions/generic-host

さらに、ソースコードもそんなに完全に理解しようとしなければ追うのも難しくないと思うので興味がある人は dotnet/runtime のリポジトリの以下のフォルダーも見ておくといいと思います。

ということで以上になります。

Microsoft (有志)

Discussion