.NET での汎用ホスト

この記事では、Microsoft.Extensions.Hosting NuGet パッケージで使用できる .NET 汎用ホストを構成および構築するためのさまざまなパターンについて学習します。 .NET 汎用ホストが担当するのは、アプリの起動および有効期間の管理です。 Worker サービス テンプレートを使用すると、.NET 汎用ホスト HostApplicationBuilder が作成されます。 汎用ホストは、コンソール アプリなど、他の種類の .NET アプリケーションで使用できます。

''ホスト'' とは、次のようなアプリのリソースと有効期間機能をカプセル化するオブジェクトです。

  • 依存関係の挿入 (DI)
  • ログの記録
  • 構成
  • アプリのシャットダウン
  • IHostedService の実装

ホストが起動すると、サービス コンテナーのホステッド サービスのコレクションに登録されている IHostedService の各実装で IHostedService.StartAsync が呼び出されます。 Worker サービス アプリでは、BackgroundService インスタンスを含むすべての IHostedService 実装で、BackgroundService.ExecuteAsync メソッドが呼び出されます。

アプリの相互依存するすべてのリソースを 1 つのオブジェクトに含める主な理由は、アプリの起動と正常なシャットダウンの制御の有効期間の管理のためです。

ホストを設定する

ホストは通常、Program クラス内のコードによって構成、ビルド、および実行されます。 Main メソッド:

  • CreateApplicationBuilder メソッドを呼び出して、builder オブジェクトを作成および構成します。
  • Build() を呼び出して IHost インスタンスを作成します。
  • ホスト オブジェクトに対して Run または RunAsync メソッドを呼び出します。

.NET Worker サービス テンプレートを使用すると、汎用ホストを作成する次のコードが生成されます。

using Example.WorkerService;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();

IHost host = builder.Build();
host.Run();

ワーカー サービスの詳細については、「.NET のワーカー サービス」を参照してください。

ホスト ビルダーの設定

CreateApplicationBuilder メソッド:

  • GetCurrentDirectory() によって返されるパスにコンテンツ ルートを設定します。
  • 次から ホスト構成を読み込みます。
    • プレフィックス DOTNET_ が付いた環境変数。
    • コマンド ライン引数。
  • 次からアプリの構成を読み込みます。
    • appsettings.json
    • appsettings.{Environment}.json
    • Development 環境でアプリが実行される場合に使用されるシークレット マネージャー。
    • 環境変数。
    • コマンド ライン引数。
  • 次のログ プロバイダーを追加します。
    • コンソール
    • デバッグ
    • EventSource
    • イベント ログ (Windows で実行されている場合のみ)
  • 環境が Development である場合は、スコープの検証と依存関係の検証を有効にします。

HostApplicationBuilder.ServicesMicrosoft.Extensions.DependencyInjection.IServiceCollection インスタンスです。 これらのサービスは、登録されたサービスを解決するために依存関係の注入で使用される IServiceProvider を構築するために使用されます。

フレームワークが提供するサービス

IHostBuilder.Build() または HostApplicationBuilder.Build() のどちらかを呼び出すと、次のサービスが自動的に登録されます。

IHostApplicationLifetime

起動後タスクとグレースフル シャットダウン タスクを処理するために IHostApplicationLifetime サービスを任意のクラスに注入します。 インターフェイス上の 3 つのプロパティは、アプリの起動およびアプリの停止のイベント ハンドラー メソッドを登録するために使用されるキャンセル トークンです。 インターフェイスには StopApplication() メソッドも含まれています。

次の例は、IHostApplicationLifetime イベントを登録する IHostedServiceIHostedLifecycleService の実装です。

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace AppLifetime.Example;

public sealed class ExampleHostedService : IHostedService, IHostedLifecycleService
{
    private readonly ILogger _logger;

    public ExampleHostedService(
        ILogger<ExampleHostedService> logger,
        IHostApplicationLifetime appLifetime)
    {
        _logger = logger;

        appLifetime.ApplicationStarted.Register(OnStarted);
        appLifetime.ApplicationStopping.Register(OnStopping);
        appLifetime.ApplicationStopped.Register(OnStopped);
    }

    Task IHostedLifecycleService.StartingAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("1. StartingAsync has been called.");

        return Task.CompletedTask;
    }

    Task IHostedService.StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("2. StartAsync has been called.");

        return Task.CompletedTask;
    }

    Task IHostedLifecycleService.StartedAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("3. StartedAsync has been called.");

        return Task.CompletedTask;
    }

    private void OnStarted()
    {
        _logger.LogInformation("4. OnStarted has been called.");
    }

    private void OnStopping()
    {
        _logger.LogInformation("5. OnStopping has been called.");
    }

    Task IHostedLifecycleService.StoppingAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("6. StoppingAsync has been called.");

        return Task.CompletedTask;
    }

    Task IHostedService.StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("7. StopAsync has been called.");

        return Task.CompletedTask;
    }

    Task IHostedLifecycleService.StoppedAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("8. StoppedAsync has been called.");

        return Task.CompletedTask;
    }

    private void OnStopped()
    {
        _logger.LogInformation("9. OnStopped has been called.");
    }
}

ExampleHostedService 実装を追加するように Worker サービス テンプレートを変更することができます。

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using AppLifetime.Example;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddHostedService<ExampleHostedService>();
using IHost host = builder.Build();

await host.RunAsync();

アプリケーションにより、次のサンプル出力が書き込まれます。

// Sample output:
//     info: AppLifetime.Example.ExampleHostedService[0]
//           1.StartingAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           2.StartAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           3.StartedAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           4.OnStarted has been called.
//     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: ..\app-lifetime\bin\Debug\net8.0
//     info: AppLifetime.Example.ExampleHostedService[0]
//           5.OnStopping has been called.
//     info: Microsoft.Hosting.Lifetime[0]
//           Application is shutting down...
//     info: AppLifetime.Example.ExampleHostedService[0]
//           6.StoppingAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           7.StopAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           8.StoppedAsync has been called.
//     info: AppLifetime.Example.ExampleHostedService[0]
//           9.OnStopped has been called.

出力には、さまざまなライフサイクル イベントすべての順序が表示されます。

  1. IHostedLifecycleService.StartingAsync
  2. IHostedService.StartAsync
  3. IHostedLifecycleService.StartedAsync
  4. IHostApplicationLifetime.ApplicationStarted

たとえば、Ctrl+C キーを使ってアプリケーションを停止すると、次のイベントが発生します。

  1. IHostApplicationLifetime.ApplicationStopping
  2. IHostedLifecycleService.StoppingAsync
  3. IHostedService.StopAsync
  4. IHostedLifecycleService.StoppedAsync
  5. IHostApplicationLifetime.ApplicationStopped

IHostLifetime

IHostLifetime 実装では、ホストを開始および停止するタイミングが制御されます。 登録されている最後の実装が使用されます。 Microsoft.Extensions.Hosting.Internal.ConsoleLifetime は、既定の IHostLifetime 実装です。 シャットダウンの有効期間のしくみの詳細については、「ホストのシャットダウン」を参照してください。

IHostLifetime インターフェイスは IHostLifetime.WaitForStartAsync メソッドを公開しています。これは IHost.StartAsync の開始時に呼び出され、完了するまで待機してから続行されます。 これを使って、外部イベントによって通知されるまで開始を遅らせることができます。

さらに、IHostLifetime インターフェイスは IHostLifetime.StopAsync メソッドを公開しています。これは、ホストが停止し、シャットダウンするタイミングであることを示すために、IHost.StopAsync から呼び出されます。

IHostEnvironment

次の設定に関する情報を取得するため、クラスに IHostEnvironment サービスを注入します。

さらに、IHostEnvironment サービスでは、次の拡張メソッドを使用して環境を評価する機能が公開されます。

ホストの構成

ホスト構成は、IHostEnvironment 実装のプロパティを構成するために使用されます。

ホストの構成は IHostApplicationBuilder.Configuration プロパティで使用でき、環境の実装は IHostApplicationBuilder.Environment プロパティで使用できます。 ホストを構成するには、Configuration プロパティにアクセスし、使用可能な拡張メソッドのいずれかを呼び出します。

ホストの構成を追加するため、次の例を考えてみましょう。

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Environment.ContentRootPath = Directory.GetCurrentDirectory();
builder.Configuration.AddJsonFile("hostsettings.json", optional: true);
builder.Configuration.AddEnvironmentVariables(prefix: "PREFIX_");
builder.Configuration.AddCommandLine(args);

using IHost host = builder.Build();

// Application code should start here.

await host.RunAsync();

上記のコードでは次の操作が行われます。

  • GetCurrentDirectory() によって返されるパスにコンテンツ ルートを設定します。
  • 次から ホスト構成を読み込みます。
    • hostsettings.json.
    • プレフィックス PREFIX_ が付いた環境変数。
    • コマンド ライン引数。

アプリの構成

アプリの構成は、IHostApplicationBuilder 上で ConfigureAppConfiguration を呼び出すことで作成されます。 public IHostApplicationBuilder.Configuration プロパティを使うと、コンシューマーは使用できる拡張メソッドを使って既存の構成を読み取ったり、既存の構成を変更したりできます。

詳細については、「.NET での構成」を参照してください。

ホストのシャットダウン

ホステッド プロセスを停止する方法はいくつかあります。 最も一般的には、ホステッド プロセスは次の方法で停止することができます。

  • 他のユーザーが RunHostingAbstractionsHostExtensions.WaitForShutdown を呼び出しておらず、Main が完了し、アプリが正常に終了した場合。
  • アプリがクラッシュした場合。
  • SIGKILL (または CTRL+Z) を使用して、アプリが強制的にシャットダウンされる場合。

ホスティング コードは、これらのシナリオの処理に関知しません。 プロセスの所有者は、他のアプリケーションと同じようにそれらを処理する必要があります。 ホステッド サービス プロセスを停止するための他のいくつかの方法を次に示します。

  • ConsoleLifetime が使用されている場合は (UseConsoleLifetime)、これは次のシグナルをリッスンし、ホストの正常な停止を試みます。
    • SIGINT (または CTRL+C)。
    • SIGQUIT (または Windows の場合は CTRL+BREAK、Unix の場合は CTRL+\)。
    • SIGTERM (docker stop など、他のアプリによって送信されるもの)。
  • アプリによって Environment.Exit が呼び出された場合。

組み込みのホスティング ロジック (具体的には ConsoleLifetime クラス) がこれらのシナリオを処理します。 ConsoleLifetime では、アプリケーションを正常に終了できるように、''シャットダウン'' シグナルである SIGINT、SIGQUIT、および SIGTERM の処理を試みます。

.NET 6 より前では、.NET コードで SIGTERM を適切に処理する方法がありませんでした。 この制約を回避するために、ConsoleLifetime では System.AppDomain.ProcessExit をサブスクライブします。 ProcessExit が発生すると、ConsoleLifetime ではホストに停止するよう合図し、ProcessExit スレッドをブロックし、ホストが停止するのを待機します。

プロセス終了ハンドラーではアプリケーションのクリーンアップ コードを実行できます。たとえば、IHost.StopAsyncMain メソッドの HostingAbstractionsHostExtensions.Run の後のコードです。

しかし、SIGTERM は ProcessExit を発生させる唯一の方法ではなかったため、このアプローチには他にも問題がありました。 SIGTERM は、アプリ コードが Environment.Exit を呼び出すときにも発生します。 Environment.Exit は、Microsoft.Extensions.Hosting アプリ モデル内のプロセスをシャットダウンする適切な方法ではありません。 ProcessExit イベントを発生させてから、プロセスを終了します。 Main メソッドの末尾は実行されません。 バックグラウンドおよびフォアグラウンド スレッドは終了し、finally ブロックは実行 "されません"。

ホストがシャットダウンするのを待機している間、ConsoleLifetime によって ProcessExit がブロックされるため、この動作により、Environment.Exit からのデッドロックが発生し、またブロックされて、ProcessExit の呼び出しを待機することになります。 さらに、SIGTERM 処理でプロセスの正常なシャットダウンを試みていたため、ConsoleLifetimeExitCode0 に設定され、Environment.Exit に渡されるユーザーの終了コードが上書きされました。

.NET 6 では、POSIX シグナルがサポートされ、処理されます。 ConsoleLifetime は SIGTERM を適切に処理し、Environment.Exit が呼び出されると関与しなくなります。

ヒント

.NET 6 以上の場合、ConsoleLifetime に、シナリオ Environment.Exit を処理するロジックがなくなりました。 Environment.Exit を呼び出し、クリーンアップ ロジックを実行する必要があるアプリでは、それら自体で ProcessExit をサブスクライブできます。 これらのシナリオでは、ホスティングはホストの正常な停止を試みなくなります。

アプリケーションでホスティングが使用されており、ホストを正常に停止したい場合は、Environment.Exit ではなく IHostApplicationLifetime.StopApplication を呼び出すことができます。

ホスティング シャットダウン プロセス

次のシーケンス図は、ホスティング コードで内部的にシグナルがどのように処理されるのかを示しています。 ほとんどのユーザーは、このプロセスを理解する必要はありません。 しかし、深く理解する必要がある開発者にとっては、良いビジュアルが作業を開始するのに役立つ場合があります。

ホストが開始された後、ユーザーが Run または WaitForShutdown を呼び出した場合、ハンドラーが IApplicationLifetime.ApplicationStopping に登録されます。 WaitForShutdown で実行が一時停止し、ApplicationStopping イベントが発生するのを待っています。 Main メソッドはすぐに return をせず、アプリは Run または WaitForShutdown が return するまで実行中のままです。

シグナルがプロセスに送信された場合、次のシーケンスが開始されます。

ホスティング シャットダウン シーケンス図。

  1. 制御は ConsoleLifetime から ApplicationLifetime に流れ、ApplicationStopping イベントを発生させます。 このようにして、WaitForShutdownAsync に対して Main 実行コードのブロックを解除するよう合図します。 一方、POSIX シグナル ハンドラーは Cancel = true で return します。これは POSIX シグナルが処理されたためです。
  2. Main 実行コードではもう一度実行を開始し、ホストに StopAsync() を指示し、その後、ホストされているサービスをすべて停止し、その他の停止イベントを発生させます。
  3. 最後に、WaitForShutdown が終了し、何らかのアプリケーション クリーンアップ コードの実行が可能になり、Main メソッドは正常に終了できます。

Web サーバー シナリオでのホストのシャットダウン

Kestrel においては、HTTP/1.1 プロトコルと HTTP/2 プロトコルの両方に関して、およびトラフィックをスムーズにドレインするためのロード バランサーを備えたさまざまな環境でそれを構成する方法に関して、正常なシャットダウンが機能する一般的なシナリオが他にもたくさんあります。 Web サーバーの構成はこの記事の範囲外ですが、ASP.NET Core Kestrel Web サーバーのオプションの構成に関するドキュメントで詳細を確認できます。

ホストは、シャットダウン信号 (たとえば、CTL+CStopAsync など) を受信すると、ApplicationStopping の信号を発することでアプリケーションに通知します。 正常に終了する必要がある実行時間の長い操作がある場合は、このイベントをサブスクライブする必要があります。

次に、ホストは構成が可能なシャットダウン タイムアウト (既定では 30 秒) で IServer.StopAsync を呼び出します。 Kestrel (および Http.Sys) は、ポート バインドを閉じ、新しい接続の受け入れを停止します。 また、現在の接続に対して新しい要求の処理を停止するように指示します。 HTTP/2 および HTTP/3 の場合、暫定 GOAWAY メッセージがクライアントに送信されます。 HTTP/1.1 の場合、要求は順番に処理されるため、接続ループを停止します。 IIS の動作は異なり、503 状態コードで新しい要求を拒否します。

アクティブな要求はシャットダウン タイムアウトが完了するまで制御を保持します。 これらがすべてタイムアウト前に完了した場合、サーバーはすぐに制御をホストに返します。 タイムアウトの時間になると、保留中の接続と要求が強制的に中止され、ログとクライアントへのエラーの原因となる可能性があります。

Load Balancer に関する考慮事項

ロード バランサーを使用するときにクライアントを新しい宛先にスムーズに移行させるには、次の手順に従います。

  • 新しいインスタンスを起動し、それに対してトラフィックの分散を開始します (スケーリング目的で複数のインスタンスを既に持っている場合があります)。
  • ロード バランサー構成の中の古いインスタンスが新しいトラフィックの受信を停止するように、これを無効化または削除します。
  • 古いインスタンスにシャットダウンを行うよう信号を送ります。
  • それがドレインまたはタイムアウトするのを待ちます。

関連項目