白猫のメモ帳

C#とかJavaとかJavaScriptとかHTMLとか機械学習とか。

.NETの汎用ホストでコンソールアプリケーションを作ってみる

.NET Framework(4.6.2)のコンソールアプリケーションを.NET(7)にマイグレーションしてみる - 白猫のメモ帳

前回.NET Frameworkから.NETへのマイグレーションをした際にごにょごにょした、設定の読み込みだったりログだったりを便利に使える汎用ホストという機能があるらしいので使ってみます。
いつからあるのかよくわからなかったのですが、.NET Core 3.0あたりからなんでしょうか…。

なんだかいろいろなことができてしまってよくわからないやつなのですが、MSのドキュメントによれば、

''ホスト'' とは、次のようなアプリのリソースと有効期間機能をカプセル化するオブジェクトです。
依存関係の挿入 (DI)
ログの記録
構成
アプリのシャットダウン
IHostedService の実装

だそうです。
.NET 汎用ホスト - .NET | Microsoft Learn

どうやって使うの?

ははーんなるほどこの汎用ホストってやつを使えば設定ファイル読み込みだったりDIだったりもできちゃうわけか、そいつは便利だと思ってドキュメントをざっと読むのですが、使い方がわからなくてフリーズします。
なんだかユーティリティ的なものだと思って読んでいたのが良くなかったのかもしれません。
(汎用ホストって名前汎用的すぎて何を指しているのかがよくわからない)

IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices((hostContext, services) =>
    {
        services.AddHostedService<Worker>();
    })
    .Build();

host.Run();

このコードに対して、Hostが作れたのはいいけど私のコードはどこに書くんだって思った時点でそもそもの発想が間違っていたようです。
よく見るとWorkerというクラスがジェネリック指定されてAddHostedServiceというメソッドが呼ばれています。
つまり、このサービスクラスをホストしてねってことのようです。

で、ホストってなんだって言うとここに戻ってきます。

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

なるほどね?
ホストって言葉ってよく聞くんですが、正しく意味を答えろって言われると難しい気がします。コンピュータとかサーバと同義みたいな。
じゃあホストするって?
うーん…サービスを提供できる場所に置くみたいなイメージですかね。ホストはサービスを提供する「何か」かな。
なんかこのあたりの定義結構難しいですね。

まぁ汎用ホストは動かしたいサービスを載せておいて実行するのをサポートしてくれる「何か」なんですかね。
汎用だからこそ汎用ホスト自体の説明をもう少ししてほしい…。

使ってみる

AddHostedServiceを使えば良さそうなことがわかったので使ってみます。
IHostedServiceというインタフェースの実装を作れば良さそう。

求められるのはStartAsyncとStopAsyncの2メソッド。
開始と終了…RunとかExecute的なものじゃないようです。
とりあえずStartAsync側に処理を書いてみます。

class TestService : IHostedService
{
    public Task StartAsync(CancellationToken cancellationToken)
    {
        Debug.WriteLine("なんか処理するよ");
        return Task.CompletedTask;

    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }
}

で、実行してみると、

using var host = Host.CreateDefaultBuilder(args)
                     .ConfigureServices((hostContext, services) => services.AddHostedService<TestService>())
                     .Build();
host.Run();

実行が終わりません。
画像の通りCtrl+Cを押すと止まります。

もう一回説明を見に行きます。
アプリの起動と正常なシャットダウンの制御とあるので、自分でシャットダウンしないとバックグラウンドサービス的に動くんですね。

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

アプリの相互依存するすべてのリソースを 1 つのオブジェクトに含める主な理由は、アプリの起動と正常なシャットダウンの制御の有効期間の管理のためです。 これを実行するには、Microsoft.Extensions.Hosting NuGet パッケージを使用します。

IHostApplicationLifetimeでホストの終了をコントロール

.NET 汎用ホスト - .NET | Microsoft Learn

なんだかいろいろ書いてありますが、最後に

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

との記載があるのでこの通りにします。
つまりこう。

class TestService : IHostedService
{
    private readonly IHostApplicationLifetime appLifetime;

    public TestService(IHostApplicationLifetime appLifetime)
    {
        this.appLifetime = appLifetime;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        Debug.WriteLine("なんか処理するよ");
        this.appLifetime.StopApplication();
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }
}

IHostApplicationLifetimeはコンストラクタに引数で設定しておけば勝手にDIしてくれます。
Startの中でStopするのちょっと気持ち悪いけどしょうがないよね。

LoggerとかConfigurationはどこから

ログの記録、構成も便利にしてくれるらしいのですが、どこから来るのでしょうか。
これはIHostApplicationLifetimeと一緒でコンストラクタに引数設定しておくと勝手にDIしてくれます。

class TestService : IHostedService
{
    private readonly IHostApplicationLifetime appLifetime;
    private readonly ILogger logger;
    private readonly IConfiguration confing;

    public TestService(IHostApplicationLifetime appLifetime, ILogger<TestService> logger, IConfiguration confing)
    {
        this.appLifetime = appLifetime;
        this.logger = logger;
        this.confing = confing;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        this.logger.LogInformation("start");
        this.logger.LogInformation(this.confing.GetValue<string>("SomethingConfiguration"));
        this.appLifetime.StopApplication();
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        this.logger.LogInformation("stop");
        return Task.CompletedTask;
    }
}

ついでに普通のDIも

AddHostedServiceでサービスを登録する際に併せてAddScopedでインタフェースとその実装クラスを指定しておくと、
こちらもコンストラクタインジェクションをしてくれます。

using var host = Host.CreateDefaultBuilder(args)
                     .ConfigureServices((hostContext, services) => services.AddHostedService<TestService>()
                                                                           .AddScoped<ISample, Sample>())
                     .Build();
host.Run();

実装クラスはコンストラクタなしかpublicで引数なしのコンストラクタを実装しておきましょう。

public interface ISample
{
    public string Hoge();
}
public class Sample : ISample
{
    public string Hoge() => "Fuga";
}

で、こうですね。

class TestService : IHostedService
{
    private readonly IHostApplicationLifetime appLifetime;
    private readonly ISample sample;

    public TestService(IHostApplicationLifetime appLifetime, ISample sample)
    {
        this.appLifetime = appLifetime;
        this.sample = sample;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        Debug.WriteLine(this.sample.Hoge());
        this.appLifetime.StopApplication();
        return Task.CompletedTask;

    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }
}

さっきからコンストラクタ引数が増減していますが、引数にあればDIしてくれる融通が利く感じで良きですね。

基底クラスとか作ってみる?

こういうの作ったら便利だったりするんですかね?

public abstract class SimpleConsoleApplicationBase<T> : IHostedService where T : SimpleConsoleApplicationBase<T>
{
    private readonly IHostApplicationLifetime appLifetime;
    protected readonly ILogger logger;
    protected readonly IConfiguration confing;

    public SimpleConsoleApplicationBase(IHostApplicationLifetime appLifetime, ILogger<T> logger, IConfiguration confing)
    {
        this.appLifetime = appLifetime;
        this.logger = logger;
        this.confing = confing;
    }

    async Task IHostedService.StartAsync(CancellationToken cancellationToken)
    {
        await this.ExecuteAsync();
        this.appLifetime.StopApplication();
    }

    Task IHostedService.StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }

    protected abstract Task ExecuteAsync();
}

で、こう。

public class TestApplication : SimpleConsoleApplicationBase<TestApplication>
{
    public TestApplication(IHostApplicationLifetime appLifetime, ILogger<TestApplication> logger, IConfiguration confing) : base(appLifetime, logger, confing)
    {
    }

    protected override Task ExecuteAsync()
    {
        base.logger.LogInformation("さんぷるだよ");
        return Task.CompletedTask;
    }
}

うーん。これでいいのかがよくわからない。

結局ユーティリティ的に使えないのかというと

普通にこういうクラスを実行したいとして、

class Simple
{
    protected private ILogger<Simple> logger;
    protected private IConfiguration confing;

    public Simple(ILogger<Simple> logger, IConfiguration confing)
    {
        this.logger = logger;
        this.confing = confing;
    }

    public void Execute()
    {
        this.logger.LogInformation("さんぷるだよ");
    }
}

GetRequiredServiceを使ったりすればRunしないで使えたりもする。

using var host = Host.CreateDefaultBuilder(args).Build();
var logger = host.Services.GetRequiredService<ILogger<Simple>>();
var config = host.Services.GetRequiredService<IConfiguration>();
new Simple(logger, config).Execute();

うーん。これはさすがに違うような気がするな。

というわけで

正解なのかわからないですが、とりあえず便利そうではあるなと思いました。
はい。