概要
- .Net Core で提供されている Generic Host(日本語だと汎用ホスト)について、何を意図したものなのか、どう使うのか、をまとめてみた。
- ただし、非常に多機能なものなので、Generic Host の生成や、サービス/DI周りにフォーカスして記載してある。
開発環境
- Visual Studio 2019 (16.4.0)
- .Net Core 3.1
背景
自分は4~5年前くらいまで.Net/C# で多少コードを書いていた(.Net 2.0 から 4.0の出始めくらいまで)。
で、最近久しぶりに .Net Core を使ってバックグラウンドで動作するアプリを作ることになったのだが、Visual Studio で以下のような感じのテンプレートコードが生成されて愕然。「ナンデスカ、コレ?」
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<Worker>();
});
}
こんな短いコードなのに何してるか1つも分からない。 元々 .Net/C# は進化が早かったけど、.Net Framework は 4.8 になり、.Net Core は 3.1 が出た令和の時代には、ここまで変わってたのかと呆然。
これが Generic Host の生成と実行のコードだということは後から分かったのだけど、そこに行きつくまでに調べたことをピックアップして以下に記載します。
Generic Host の概要
Generic Host とは、バックグラウンドプロセスやサービスプログラムといった長時間動き続けるアプリを作るための基盤となるオブジェクト。1つの処理が終わったらすぐ終了するようなコマンド等を作るためのものではない。
Generic Host はどのアプリを作る場合でも必要になる以下の機能を備えている。
- DI(Dependency Injection)コンテナ
- ログ
- ファイル、コンソール、EventLog 等への出力
- 設定の読み込み
- 設定ファイルや環境変数などからの設定の読み込み
- アプリケーションのライフサイクル管理
- アプリ開始時の初期化や終了前のクリーンアップ処理の実行など
つまり Generic Host は「どのアプリでも大体必要になることは用意してあげるので、あなたは自分のロジックに集中してね」と言ってくれる有難いものである。そして、非常に拡張性が高く作られている。
そんな Generic Host は実体としては IHost インターフェースを実装したクラスであるが、その多機能さゆえ、new でインスタンスを直接生成するのではなく、まず Generic Host を生成するための HostBuilder
を作る。そして、それに Generic Host オブジェクトの作り方を指示した後に生成してもらう、という使い方をする。いわゆる、Builderパターンだ。
ちなみに、ASP.Net Core には以前 Web Host という似たようなオブジェクトがあったが、それを汎用的にしたものが Generic Host らしい。
HostBuilder
HostBuilder の作成
先に記載したように Generic Host は HostBuilder に生成してもらうのだが、その HostBuilder 自体を作る方法は大きく分けて2つある。
その1: デフォルト設定の HostBuilder を作る
IHostBuilder builder = Host.CreateDefaultBuilder(args);
便利関数を実装した Host というクラスがあり、その static メソッドである CreateDefaultBuilder
でデフォルト設定の HostBuilder を作ることが出来る。
デフォルトの設定内容は、ドキュメント「既定の builder 設定」に書かれてるが、「.Net Coreのソース」を見る方が確実で分かりやすいと思う。環境変数やappsettings.json、ログの設定をしていることがよく分かる。以下、ごく一部を抜粋。
public static IHostBuilder CreateDefaultBuilder(string[] args)
{
var builder = new HostBuilder();
// 中略
builder.ConfigureAppConfiguration((hostingContext, config) =>
{
var env = hostingContext.HostingEnvironment;
config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
// 以下略
その2: HostBuilderを自前で作っていろいろカスタマイズする
デフォルト設定では要件に合わない場合などは自前で HostBuilder を生成して好きに構成していくことが可能。
IHostBuilder builder = new HostBuilder();
なお、HostBuilder の構成は ConfigureLogging や ConfigureAppConfiguration などの ConfigureXXX
関数を呼び出して行っていく。メソッドチェーンの形で書くとこんな感じ。
IHostBuilder builder = new HostBuilder()
.ConfigureLogging(logging =>
{
logging.AddConsole();
})
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<Worker>();
});
Generic Host の生成と実行
HostBuilder の作成・構成が一通り終わったら上記サンプルのように Build()
メソッドを呼び出して IHost を実装したオブジェクト、つまり Generic Host を生成してもらう。そして、Run() などのメソッドでそれを実行する。
IHost host = builder.Build();
host.Run();
Run() を呼んだあとは、Ctrl+C で外部から終了させるとか、アプリが自ら終了するとかしなければ、そのまま動作し続ける。
DI コンテナ
DI、および、DI コンテナとは
DI(Dependency Injection)が何かというのは詳しく書いている記事がたくさんあると思うので、ここでは概要だけ記載する。
例えば、Webサーバーからファイルをダウンロードする MyDownloader
クラスが、HTTP リクエストを送る MyHttpUtil
クラスを内部的に使って実装しているとする。例えばこんな感じ(あくまでサンプルなのでコードは適当)。
class MyHttpUtil
{
public byte[] SendGet(string uri)
{
// HTTP Get を送信して、受信したバイト配列を返す
}
}
class MyDownloader
{
// MyHttpUtil を保持
private readonly MyHttpUtil httpUtil;
public MyDownloader()
{
this.httpUtil = new MyHttpUtil();
}
// ファイルのダウンロード用メソッド
public void DownloadFile(string uri)
{
// MyHttpUtil を用いてダウンロード
var data = this.httpUtil.SendGet(uri);
// ...
}
}
この時、以下のような言い方をする。
- MyDownloader クラスは MyHttpUtil クラスに依存している
- MyHttpUtil は MyDownloader の Dependency である
- MyDownloader はクライアント、MyHttpUtil はサービス
ただ、このコードはいろいろと問題がある。
- MyHttpUtil が開発されるまで MyDownloader を動かせない
- MyHttpUtil ではなく別のクラスに置き換えたいとき、MyDownloader のコード修正が必要
- MyDownloader を単体テストしたいとき、MyHttpUtil をモックに置き換えられない
そこで、以下のように MyHttpUtil を外から MyDownloader へ渡してあげるのだが、これを Injection という。つまり、Dependency(またはサービス) を Injection しているので、このデザインパターンを Dependency Injection、略して DI と呼ぶ 。DI は依存性注入という変な?日本語に訳されてることが多いが、そんな難しい話ではない。
なお、Injection する際は、実体そのものを渡すのではなく、実体を交換できるように interface で渡してあげる。
// インターフェースを定義
interface IMyHttpUtil
{
byte[] SendGet(string uri);
}
// インターフェースの実装クラス
class MyHttpUtil : IMyHttpUtil
{
public byte[] SendGet(string uri)
{
// SendGetの実装
}
}
class MyDownloader
{
// インターフェイスを保持
private readonly IMyHttpUtil httpUtil;
// 外部から IMyHttpUtil を渡してもらう
public MyDownloader(IMyHttpUtil httpUtil)
{
this.httpUtil = httpUtil;
}
public void DownloadFile(string uri)
{
// ...
var data = this.httpUtil.SendGet(uri);
// ...
}
}
ただこれだけだと、今度は MyDownloader を作るクラスが MyHttpUtil に依存するという問題が出るので、依存性の連鎖を解決してオブジェクトを生成してくれる別の何かが必要になる。それが DI コンテナと呼ばれるフレームワークである。
.Net では Microsoft.Extensions.DependencyInjection で提供されている ServiceCollection クラスと ServiceProvider クラスがそれに該当する。詳しくは以下の記事が参考になる。
Microsoft.Extensions.DependencyInjection を使った DI の基本
サービスの Injection
Generic Host では、Injection する対象をサービスと呼んでおり、Microsoft.Extensions.DependencyInjection で提供される DIコンテナを内部で用いて管理している。このDIコンテナは Generic Host 専用のものではなく、単独でも用いることができる。
例えば、以下のようにサービスが定義されているとする。
// サービスのインターフェースの定義
interface IGreeting
{
string Greet();
}
// サービスのインターフェースの実装
class Greeting : IGreeting
{
public string Greet() => "Hello";
}
// サービスを用いる(Injectしてもらう)クラス
class Person
{
private readonly IGreeting greeting;
public Person(IGreeting greeting)
{
this.greeting = greeting;
}
public string Say() => this.greeting.Greet();
}
この時、以下のように IServiceCollection の AddXXX() メソッドでサービス、およびそれを使うクラスを登録すると、provider.GetService<Person>()
で依存性を解決したオブジェクトを取得できる。
IServiceCollection services = new ServiceCollection();
services.AddSingleton<IGreeting, Greeting>();
services.AddTransient<Person>();
var provider = services.BuildServiceProvider();
var person = provider.GetService<Person>();
Console.WriteLine(person.Say()); // Hello が表示される。
HostBuilder では ConfigureServices メソッドで登録していくが、ほとんど同じである。以下のコードの services が ServiceCollection(正確には IServiceCollection) である。
IHostBuilder builder = new HostBuilder().ConfigureServices((context, services) =>
{
services.AddSingleton<IGreeting, Greeting>();
services.AddTransient<Person>();
});
IHost host = builder.Build();
host.Run();
ただ、これだけだとサービスを登録しただけで、誰も Person を生成しようとしないので何も動作しない。
IHostedService について
ではどうするかというと、IHostedService インターフェースを実装したサービスを登録してあげる。
Generic Host は起動時に、DIコンテナーからIHostedService を実装したオブジェクトを探し、あればそのStartAsync()
メソッドを呼ぶ。終了時には同じく IHostedService の StopAsync()
メソッドを呼ぶ。
つまり、IHostService を実装したクラスが、Generic Host におけるアプリのエントリーポイントのようなものと言える。IHostedService の定義はこれだけ。
public interface IHostedService
{
/// <summary>
/// Triggered when the application host is ready to start the service.
/// </summary>
/// <param name="cancellationToken">Indicates that the start process has been aborted.</param>
Task StartAsync(CancellationToken cancellationToken);
/// <summary>
/// Triggered when the application host is performing a graceful shutdown.
/// </summary>
/// <param name="cancellationToken">Indicates that the shutdown process should no longer be graceful.</param>
Task StopAsync(CancellationToken cancellationToken);
}
これを先ほどの Person クラスに実装してみる。
※無理やりな実装だけど、サンプルなのでスミマセン。。。
// IHostedService インターフェースを実装する
class Person : IHostedService
{
private readonly IGreeting greeting;
public Person(IGreeting greeting)
{
this.greeting = greeting;
}
public string Say() => this.greeting.Greet();
// 実装を追加
public async Task StartAsync(CancellationToken cancellationToken)
{
await Task.Delay(1000);
Console.WriteLine(Say());
}
// 実装を追加
public async Task StopAsync(CancellationToken cancellationToken)
{
await Task.Delay(1000);
}
}
そして、AddHostedService() を用いて、IHostedService を実装した Person クラスを登録する。
IHostBuilder builder = new HostBuilder().ConfigureServices((context, services) =>
{
services.AddSingleton<IGreeting, Greeting>();
services.AddHostedService<Person>(); // AddHostedService()を呼ぶように変更
});
IHost host = builder.Build();
host.Run();
Run() を呼んだ延長で IHostedService を実装したオブジェクトが生成されて、StartAsync() が呼ばれる。この場合、StartAsync の中で Hello が出力される。
サービス登録用の拡張メソッドについて
サービスの登録はすでに見たように HostBuilder の ConfigureServices() を用いるのだが、各パッケージやライブラリごとに Add{サービス名} という名前のサービス登録専用拡張メソッドが用意されていることが多い。
例えば、AddMvc()
や AddGrpc()
といったものである。これらはサービス登録の複雑な手順を隠蔽してくれているので、提供されている場合はそれを使うべき。
デフォルトで Inject されるサービスについて
明示的にConfigureServices() や拡張メソッドを呼ばなくても、Generic Host として必ず登録されるサービスがある。例えば以下のもの。
.Net Core のソースを見ると、HostBuilder.Build() の延長でこれらは自動で登録されていることが分かる。
private void CreateServiceProvider()
{
var services = new ServiceCollection();
#pragma warning disable CS0618 // Type or member is obsolete
services.AddSingleton<IHostingEnvironment>(_hostingEnvironment);
#pragma warning restore CS0618 // Type or member is obsolete
services.AddSingleton<IHostEnvironment>(_hostingEnvironment);
services.AddSingleton(_hostBuilderContext);
// register configuration as factory to make it dispose with the service provider
services.AddSingleton(_ => _appConfiguration);
#pragma warning disable CS0618 // Type or member is obsolete
services.AddSingleton<IApplicationLifetime>(s => (IApplicationLifetime)s.GetService<IHostApplicationLifetime>());
#pragma warning restore CS0618 // Type or member is obsolete
services.AddSingleton<IHostApplicationLifetime, ApplicationLifetime>();
services.AddSingleton<IHostLifetime, ConsoleLifetime>();
services.AddSingleton<IHost, Internal.Host>();
services.AddOptions();
services.AddLogging();
これらのサービスは、アプリの起動時や終了時にイベント通知をしてもらったり、アプリ名やアプリのルートディレクトリ等の環境情報を取得したりするもの。詳しくはリンク先のドキュメントを参照。
最後に
Generic Host のログ、設定、ライフサイクル管理については全然書けなかったので、また、機会があれば記載したいです。