Generic Host(汎用ホスト)は、.NETアプリに依存関係の注入(DI)・構成(appsettings.jsonや環境変数)・ログ・起動と停止の管理をまとめて提供する仕組みです。ASP.NET Coreの土台と同じものを、コンソールアプリや常駐ワーカーでも使えます。この記事では、.NET 10のテンプレートが使うHost.CreateApplicationBuilderを前提に、既定で何が読み込まれ何が有効になるかを実行結果で確かめ、例外で止まったワーカーがサービス管理から「正常終了」に見えてしまう落とし穴と、その対処まで扱います。
検証環境は macOS(Intel Core i9-9880H)、.NET SDK 10.0.401、dotnet new workerが参照する Microsoft.Extensions.Hosting 10.0.12 です(.NET 10ランタイムの最新パッチは2026年9月8日公開の10.0.12、サポート終了は2028年11月14日)。
まとめ:Generic Hostで押さえる要点
- 新規コードは
Host.CreateApplicationBuilder(args)で始めます。.NET 7で追加されたプロパティ形式の書き方で、従来のHost.CreateDefaultBuilder(コールバック形式)と既定の中身はほぼ同じです。 - 設定はappsettings.json → appsettings.{環境名}.json →(Developmentのみ)User Secrets → 環境変数 → コマンドライン引数の順に読まれ、後のものが勝ちます。.NET 10からは
<アプリ名>.settings.jsonも読み込まれます。 - content root(appsettings.jsonを探す場所)は既定でカレントディレクトリです。別の場所から起動すると設定ファイルが黙って読まれません。
- DIのスコープ検証(Singletonが Scopedを握る誤り)は、既定ではDevelopment環境でだけ有効になります。本番環境の既定設定ではホストの構築がそのまま通ります。
- 既定のログ出力先はConsole・Debug・EventSource・EventLog(Windowsのみ)で、ファイル出力は含まれません。
- BackgroundServiceで例外が出るとホストは停止しますが、プロセスの終了コードは0のままです。systemdに再起動させたいなら
Environment.ExitCodeに0以外を入れてから止めます。Windowsサービスは判定の仕組みが違い、終了コードだけでは回復が動きません。
Generic Host(汎用ホスト)の役割と「.NET host」との区別
ホストは、アプリが使う部品を1つのオブジェクトに束ねたものです。Microsoft Learnの説明では、DI、ログ、構成、アプリのシャットダウン、IHostedServiceの実装をカプセル化するオブジェクトとされています。ASP.NET Coreと同じbuilder.Servicesへの登録やILogger<T>の注入が、バッチや常駐処理でもそのまま使えます。DIコンテナそのものの考え方はIoCコンテナとは?制御の反転の仕組みとSpring IoCコンテナ・Beanをわかりやすく解説で扱っています。
名前が似ている「.NET host」「dotnet-host」「Hosting Bundle」との違い
「.NET host」で検索すると、Generic Host以外に名前の似た別物が3つ混ざります。この記事で扱うのは表の1行目です。
| 名前 | 正体 | 使う場面 |
|---|---|---|
| Generic Host | Microsoft.Extensions.Hosting のライブラリ | アプリ内のDI・設定・ログ・起動停止 |
| dotnet(muxer) | ランタイムやSDKを起動するネイティブ実行ファイル | dotnet runやdotnet app.dll |
| dotnet-host | Linuxのmuxer入りパッケージ名 | aptやdnfでの.NET導入時の依存 |
| Hosting Bundle | .NETランタイムとASP.NET Core Moduleのインストーラー | IISでASP.NET Coreを動かす |
CreateApplicationBuilderとCreateDefaultBuilderの違い
HostApplicationBuilder(Host.CreateApplicationBuilderが返す型)は dotnet/runtime の release/7.0 ブランチで初めて登場し、.NET 7から使えます。Microsoft Learnの汎用ホストのページは「.NET 6で導入」と書いていますが、release/6.0 のソースには該当ファイルがありません。既定で行う初期化の内容は両者で同じで、違うのは書き方です。
| 項目 | CreateApplicationBuilder | CreateDefaultBuilder |
|---|---|---|
| 戻り値 | HostApplicationBuilder | IHostBuilder |
| 書き方 | builder.Services などのプロパティ | ConfigureServices などのコールバック |
| 使える版 | .NET 7以降 | .NET Core 3.0以降 |
| テンプレート | .NET 10の worker が採用 | 古いサンプル・既存コード |
既存のCreateDefaultBuilderのコードは書き換える必要はありません。Microsoft Learnも完全にサポートされるとしたうえで、新規にはCreateApplicationBuilderを推奨しています。
dotnet new workerで作る最小構成
常駐ワーカーの雛形はdotnet new workerで作れます。
dotnet new worker -n OrderWorker
cd OrderWorker
dotnet run
生成される Program.cs は、usingを除くと処理が4文だけです。ホストを作り、BackgroundServiceを継承したWorkerを登録して、実行するだけです。
using OrderWorker;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();
var host = builder.Build();
host.Run();
プロジェクトファイルは SDK にMicrosoft.NET.Sdk.Workerを使い、Microsoft.Extensions.Hostingを参照します。コンソールアプリ(dotnet new console)から始めた場合は、dotnet add package Microsoft.Extensions.Hostingでパッケージを足し、Worker クラスを自分で作ります。Worker SDK が暗黙の using で補っている Microsoft.Extensions.Hosting・DependencyInjection・Logging・Configuration の4つの名前空間も、Program.cs に using で書く必要があります。このパッケージは .NET Framework 4.6.2 と .NET Standard 2.0 向けのビルドも同梱しているため、.NET Frameworkのアプリでも使えます。
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>dotnet-OrderWorker-(生成時のGUID)</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.12" />
</ItemGroup>
</Project>
既定で読み込まれる設定ソースと上書きの順序
Host.CreateApplicationBuilderが登録する構成ソースを .NET 10.0.12 で列挙し、主なものを整理すると次の順でした(アプリ名 OrderWorker、Development環境の例。content root 用の内部的なメモリー構成などは省略)。表の下にあるものほど優先され、同じキーを上書きします。
| 順 | ソース | 補足 |
|---|---|---|
| 1 | 環境変数(DOTNET_で始まるもの)とコマンドライン引数 | ホスト構成。環境名やcontent rootを決める |
| 2 | appsettings.json | content root直下 |
| 3 | appsettings.{環境名}.json | 例:appsettings.Production.json |
| 4 | OrderWorker.settings.json | .NET 10で追加 |
| 5 | OrderWorker.settings.{環境名}.json | .NET 10で追加 |
| 6 | User Secrets(secrets.json) | Development環境かつUserSecretsIdがあるとき |
| 7 | 環境変数(接頭辞なし) | すべての環境変数 |
| 8 | コマンドライン引数 | 引数を渡したときだけ追加 |
4と5の<アプリ名>.settings.jsonは、dotnet/runtime の PR #116987(2025年6月マージ、.NET 10に収録)で既定に加わったもので、Microsoft Learnの汎用ホストのページにはまだ載っていません。.NET 9以前のソース(release/9.0)には該当コードがありません。環境名はDOTNET_ENVIRONMENTかコマンドライン引数--environmentで決まり(両方あれば引数が優先)、どちらも無ければ Production です。ただしdotnet runは Properties/launchSettings.json の起動プロファイルを使い、worker テンプレートはそこにDOTNET_ENVIRONMENT=Developmentを書いています。検証では、シェルでDOTNET_ENVIRONMENT=Productionを付けてもdotnet runは Development のままで、--no-launch-profileを付けると Production になりました。User Secrets はプロジェクトファイルにUserSecretsIdがある場合だけ読まれます(テンプレートは生成時に付与済み。無ければdotnet user-secrets initで追加)。
設定値の型付きクラスへの読み込みと起動時検証
appsettings.json の値は、Optionsパターンで型付きのクラスに束ねるのが定石です。ValidateOnStart()を付けると、不正な値のまま起動しかけたときにホストの起動自体が失敗します。Polling__IntervalSeconds=0を与えて実行したところ、OptionsValidationExceptionでHosting failed to startとなり、ワーカーは1回も動きませんでした。
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"Polling": { "IntervalSeconds": 30 }
}
登録部分の抜粋です(ワーカーを含む全体は後述)。
using Microsoft.Extensions.Options;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddOptions<PollingOptions>()
.Bind(builder.Configuration.GetSection(PollingOptions.Section))
.Validate(o => o.IntervalSeconds is >= 1 and <= 3600, "Polling:IntervalSeconds は 1〜3600 秒")
.ValidateOnStart();
public sealed class PollingOptions
{
public const string Section = "Polling";
public int IntervalSeconds { get; set; } = 30;
}
値の上書きは、環境変数とコマンドライン引数で区切り文字が違う点に注意してください。環境変数では階層を__(アンダースコア2つ)、コマンドライン引数では:で表します。環境変数そのものの設定方法は環境変数とは?Linux・Windowsの設定方法とDocker・CIでの受け渡しにまとめています。
# 環境変数で上書き(階層の区切りは __)
Polling__IntervalSeconds=5 dotnet run
# コマンドライン引数で上書き(階層の区切りは :)
dotnet run -- --Polling:IntervalSeconds=5
# dotnet run は launchSettings.json の Development で動く。Production で試すなら
dotnet run --no-launch-profile
# 環境名を引数で指定する(DOTNET_ENVIRONMENT より優先)
dotnet run -- --environment Staging
content rootの既定値とWindowsサービスでの例外
appsettings.json を探す起点(content root)はDirectory.GetCurrentDirectory()です。ビルド済みの dll を別のディレクトリからdotnet /opt/app/OrderWorker.dllのように起動すると、content root が起動場所になり、appsettings.json が読まれません。検証でも /tmp から起動した場合は設定値が空のままで、エラーも警告も出ませんでした。
例外として、カレントディレクトリがC:\Windows\system32のとき(Windowsサービスや MSIX から起動された場合)だけは、実行ファイルのあるディレクトリ(AppContext.BaseDirectory)が使われます。Linuxのsystemdにはこの救済が無いため、後述のユニットファイルでWorkingDirectoryを指定します。
DIの登録とライフタイム:AddSingleton・AddScoped・AddTransient
サービスの登録はbuilder.Servicesに対して行います。有効期間(ライフタイム)は3種類です(登録部分の抜粋)。
builder.Services.AddSingleton<TimeProvider>(TimeProvider.System); // プロセスで1個
builder.Services.AddScoped<IOrderRepository, OrderRepository>(); // スコープごとに1個
builder.Services.AddTransient<OrderValidator>(); // 要求のたびに新規
builder.Services.AddHostedService<OrderPollingWorker>(); // IHostedService のシングルトンとして登録
| 登録メソッド | インスタンスの寿命 | 向いているもの |
|---|---|---|
| AddSingleton | コンテナ内で1個を共有 | HttpClientFactory、キャッシュ、時計 |
| AddScoped | スコープ1つにつき1個 | DbContext、1件の処理単位の状態 |
| AddTransient | 取り出すたびに新規 | 状態を持たない軽い処理クラス |
| AddHostedService | Singletonとして登録 | BackgroundService の実装 |
ASP.NET Coreでは HTTP リクエスト1本が自動的に1スコープになりますが、ワーカーにはリクエストがありません。処理単位のスコープが自動では作られず、ルートのプロバイダーから Scoped のサービスを解決するとプロセスが終わるまで同じインスタンスが残る、というのがWebアプリとの最大の違いです。
BackgroundServiceからScopedのサービスを使う方法
AddHostedServiceはワーカーを Singleton として登録します。そのコンストラクタで Scoped のサービス(たとえば Entity Framework Core の DbContext)を直接受け取ると、Singleton が Scoped を握り続ける「captive dependency」になり、1個の DbContext をプロセスが終わるまで使い回すことになります。
この誤りを検出するスコープ検証(ValidateScopesとValidateOnBuild)は、既定ではDevelopment環境でだけ有効になります。実際に Scoped を直接受け取るワーカーを登録してホストをBuild()すると、Development では実行時にCannot consume scoped service 'ScopedDep' from singleton 'Microsoft.Extensions.Hosting.IHostedService'.で失敗し、Production では何も言わずに構築が通りました。テンプレートのままdotnet runすれば起動プロファイルで Development になるため、ローカルで一度起動しておけば検出できます。本番環境でも検証したい場合は、builder.ConfigureContainer(new DefaultServiceProviderFactory(new ServiceProviderOptions { ValidateScopes = true, ValidateOnBuild = true }))を呼ぶと Production でも同じ例外で止まることを確認しました。
正しい書き方は、IServiceScopeFactoryを受け取り、処理1回ごとにCreateScope()でスコープを作ってその中で解決する方法です。「BackgroundServiceの実装と起動・停止の順序」の章の全体コードで、ループ1周ごとにOrderRepositoryが新しく作られることを確認しています。
ILoggerでのログ出力と既定のプロバイダー
ログはILogger<T>をコンストラクタで受け取り、LogInformationなどで書きます。メッセージに{Pending}のようなプレースホルダーを使うと、値が文字列に埋め込まれるだけでなく、構造化ログの項目名としても残ります。文字列補間($"...")で組み立てると、この項目名が失われます。
Host.CreateApplicationBuilderが既定で追加するログの出力先(プロバイダー)は次の4つです。macOS上の実測では Console・Debug・EventSource の3つが登録されていました。
- Console:標準出力。コンテナやjournalで拾うならこれで足ります
- Debug:デバッガーの出力ウィンドウ
- EventSource:dotnet-trace などの診断ツール向け
- EventLog:Windowsのイベントログ。Windowsのときだけ追加され、既定では Warning 以上だけを書きます
出力レベルと形式は appsettings.json のLoggingセクションで変えられます。キーはカテゴリ名(通常は名前空間とクラス名)の前方一致で、FormatterNameを json にすると1行1件のJSONで出力されます。環境変数Logging__LogLevel__OrderPollingWorker=Warningを与えたときは、Information の「未処理の注文」ログが1件も出なくなることを確認しました。
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information",
"OrderPollingWorker": "Debug"
},
"Console": {
"FormatterName": "json"
}
}
}
ログのファイル出力とサードパーティ製プロバイダー
組み込みのプロバイダーにはファイル出力がありません。ファイルに書くなら Serilog や NLog などのサードパーティ製プロバイダーを追加します。常駐ワーカーなら、標準出力に JSON で出して journal やコンテナのログ基盤に集める方が、ローテーションを自前で持つより手間が少なく済みます。ファイル出力が要るのは、使っている収集エージェントがファイルしか読めない場合や、監査のためにサーバー上へ一定期間残す決まりがある場合です。
BackgroundServiceの実装と起動・停止の順序
ここまでの要素(Options・Scoped・ILogger)を組み合わせた Program.cs の全体が次のコードです(dotnet new workerのプロジェクトにそのまま置けば動きます)。PeriodicTimerで一定間隔ごとに処理し、停止要求(stoppingToken)が来たらループを抜けます。Polling:IntervalSeconds を1にして動かすと、1秒ごとに「OrderRepository #1 created」「#2」「#3」と出力され、スコープが1周ごとに切れていることを確認できました。
using Microsoft.Extensions.Options;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddOptions<PollingOptions>()
.Bind(builder.Configuration.GetSection(PollingOptions.Section))
.Validate(o => o.IntervalSeconds is >= 1 and <= 3600, "Polling:IntervalSeconds は 1〜3600 秒")
.ValidateOnStart();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddHostedService<OrderPollingWorker>();
var host = builder.Build();
host.Run();
public sealed class PollingOptions
{
public const string Section = "Polling";
public int IntervalSeconds { get; set; } = 30;
}
public interface IOrderRepository
{
Task<int> CountPendingAsync(CancellationToken ct);
}
// 動作確認用のダミー実装(実際はDBなどに問い合わせる)
public sealed class OrderRepository : IOrderRepository
{
private static int _created;
public OrderRepository(ILogger<OrderRepository> logger) =>
logger.LogInformation("OrderRepository #{Number} created", Interlocked.Increment(ref _created));
public Task<int> CountPendingAsync(CancellationToken ct) => Task.FromResult(3);
}
public sealed class OrderPollingWorker(
IServiceScopeFactory scopeFactory,
IOptions<PollingOptions> options,
IHostApplicationLifetime lifetime,
ILogger<OrderPollingWorker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(options.Value.IntervalSeconds));
try
{
do
{
// 1周ごとにスコープを作り、Scoped のサービスをその中で解決する
using var scope = scopeFactory.CreateScope();
var repo = scope.ServiceProvider.GetRequiredService<IOrderRepository>();
var pending = await repo.CountPendingAsync(stoppingToken);
logger.LogInformation("未処理の注文: {Pending} 件", pending);
}
while (await timer.WaitForNextTickAsync(stoppingToken));
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
// Ctrl+C や SIGTERM による正常な停止
}
catch (Exception ex)
{
logger.LogCritical(ex, "ポーリングを継続できないため停止します");
Environment.ExitCode = 1; // systemd に異常終了を伝える(Windowsサービスは後述)
lifetime.StopApplication(); // 停止処理(StopAsync など)は通常どおり走らせる
}
}
}
ライフサイクルイベントが呼ばれる順序
.NET 8で追加されたIHostedLifecycleServiceを実装すると、開始と停止の前後に処理を差し込めます。IHostApplicationLifetimeのイベントと合わせた順序は次のとおりで、Microsoft Learnの記載と .NET 10.0.12 での出力が一致しました。
| 起動時 | 停止時(Ctrl+C / SIGTERM) |
|---|---|
| 1. StartingAsync | 1. ApplicationStopping |
| 2. StartAsync | 2. StoppingAsync |
| 3. StartedAsync | 3. StopAsync(ExecuteAsync にキャンセル通知) |
| 4. ApplicationStarted | 4. StoppedAsync |
| – | 5. ApplicationStopped |
表にExecuteAsyncが無いのは、.NET 10で位置が固定されなくなったためです。.NET 10の破壊的変更で、BackgroundService は StartAsync の中で ExecuteAsync 全体をTask.Runで別スレッドに渡すようになりました。.NET 9までは最初の await より前の同期部分がメインスレッドで走り、他のサービスの起動を止めていました。手元の実行では StartedAsync より先に ExecuteAsync の1行目が出力されましたが、スレッドの都合で入れ替わりうる順序です。起動前に同期的に済ませたい処理は StartAsync をオーバーライドしてbase.StartAsyncの前に書くか StartingAsync に置き、全サービスの起動後に始めたい処理は ExecuteAsync の先頭で ApplicationStarted を待ちます。
停止には猶予時間があり、HostOptions.ShutdownTimeoutの既定は30秒です(.NET 10.0.12で実測)。30秒経つと各サービスの StopAsync に渡された CancellationToken がキャンセルされますが、トークンを見ない処理が強制終了されるわけではありません。停止処理の側でもトークンに応答するように書き、長いバッチを途中で止めない設計にしたい場合はこの値を延ばして、systemd 側のTimeoutStopSecもそれより長くします。複数のワーカーの開始と停止は既定で1つずつ順番に行われ(ServicesStartConcurrentlyとServicesStopConcurrentlyは false)、停止は登録と逆の順で進みます。
例外で止まったワーカーが「正常終了」になる問題と終了コード
ExecuteAsyncから例外が漏れたときの既定の動作は、.NET 6以降BackgroundServiceExceptionBehavior.StopHostです。ホストが停止し、ログにはThe HostOptions.BackgroundServiceExceptionBehavior is configured to StopHost.が critical で出ます。問題はその後で、実測するとプロセスの終了コードは0でした。RunAsyncも例外を投げずに戻ります。
systemd は終了コードで成否を判断するため、終了コード0ならRestart=on-failureは再起動しません。Windowsサービスも同様に回復しません。Microsoft LearnのWindowsサービスのページは、StopHost ではホストがクリーンに停止するため、Windowsサービス管理システムはサービスを再起動しないと明記しています。プロセスの生死や再起動回数しか見ていない監視では、ワーカーが止まったことを検出できません。
systemd で動かす場合は、前章のコードのようにEnvironment.ExitCode = 1を設定してからStopApplication()を呼びます。macOS での実行では、停止処理を通常どおり走らせたままプロセスの終了コードが1になりました。
Windowsサービスでは、この方法は効きません。回復オプションが動くのは、既定ではサービスが SERVICE_STOPPED を報告せずにプロセスが終了したときだけで、ホストがクリーンに止まるとサービスは停止を報告してしまうからです。Microsoft Learnのサンプルは catch 句でEnvironment.Exit(1)を呼んでおり、Windowsではこちらに従います(本記事では Windows 実機での回復動作までは検証していません)。ただし汎用ホストのページにあるとおり、.NET 6以降はEnvironment.Exitを呼ぶとホストは正常な停止処理を試みないため、他のワーカーの StopAsync やバッファされたログの書き出しは飛ばされます。
BackgroundServiceExceptionBehavior.Ignoreに変える方法は勧めません。例外を出したワーカーは再開されず、プロセスだけが生き残ります。.NET 6より前はこれが既定で、Microsoft Learnはこの状態を「ゾンビプロセス」と呼んでいます。監視からは生きているように見えるため、止まるより発見が遅れます。例外処理の設計全般はエラーハンドリングとは?例外処理のベストプラクティスとtry-catch-finallyの言語別の違いも参考になります。
Windowsサービス・systemdで常駐させる設定
Windowsサービスとしての登録と回復設定
Microsoft.Extensions.Hosting.WindowsServices(10.0.12)を追加し、AddWindowsServiceを呼びます。サービスとして起動されていないときは何もしないため、同じ実行ファイルをコンソールからそのまま実行してデバッグできます。
dotnet add package Microsoft.Extensions.Hosting.WindowsServices
builder.Services.AddWindowsService(options =>
{
options.ServiceName = "Order Worker";
});
登録の前に、Windows 上でdotnet publish -c Release -r win-x64 -o C:\svc\OrderWorkerのように実行ファイルを発行して配置しておきます。サービスの作成と回復オプションは管理者権限の PowerShell でsc.exeを使います。2行目は、失敗のたびに60秒後に再起動する設定です。回復オプションが動くのはプロセスが停止を報告せずに終わった場合なので、前章のとおり catch 句でEnvironment.Exit(1)を呼ぶ構成と組み合わせます。
sc.exe create "Order Worker" binpath= "C:\svc\OrderWorker\OrderWorker.exe"
sc.exe failure "Order Worker" reset= 0 actions= restart/60000/restart/60000/restart/60000
systemdユニットファイルの設定
Linuxでは Microsoft.Extensions.Hosting.Systemd を追加してAddSystemd()を呼びます。起動完了と停止開始を systemd に通知し、コンソールログを systemd の形式に変えます。通知を受け取るにはユニットファイルにType=notifyが必要です。
// dotnet add package Microsoft.Extensions.Hosting.Systemd
builder.Services.AddSystemd();
[Unit]
Description=Order Worker
[Service]
Type=notify
ExecStart=/opt/orderworker/OrderWorker
WorkingDirectory=/opt/orderworker
Environment=DOTNET_ENVIRONMENT=Production
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
WorkingDirectoryを省くと、前述の content root の仕様によって appsettings.json が読まれません。Restart=on-failureは終了コード0では働かないため、ワーカー側でEnvironment.ExitCodeを設定しておくことが前提です。
Generic Hostを使わない方がよい場面
ホストの初期化にはコストがあります。同じプログラムの中で、何もしない分岐と、ホストの構築・StartAsync・StopAsyncだけを行う分岐を Main 内の Stopwatch で5回ずつ測ると、前者は0ミリ秒、後者は506〜620ミリ秒でした(Release ビルド、JIT 実行。プロセス起動前の時間は含まない)。プロセス全体を/usr/bin/timeで測った実時間でも0.10秒と0.59秒でした。
そのため、引数を1つ受け取って結果を出力するだけの短命なCLIツールや、1回の実行が1秒未満で終わるコマンドにはホストを入れない方が素直です。この環境では構築から停止までの合計で約0.5秒が上乗せされ、DIも設定ファイルも使わない処理にはその見返りがありません。逆に、常駐する、設定を環境ごとに切り替える、DbContext などスコープ付きの依存を持つ、のどれか1つでも当てはまるなら、自前で組むよりホストに任せた方が停止処理や設定の上書き順で迷わずに済みます。gRPCサーバーのように Web ホストと組み合わせる例はgRPC C#の実装|Grpc.AspNetCoreのビルド時生成とホスティング制約を解説で扱っています。
よくある質問
Generic Hostとは何ですか?
DI・構成・ログ・起動と停止の管理をまとめて提供する Microsoft.Extensions.Hosting の仕組みです。ASP.NET Coreも内部で同じホストを使っています。
C#のコンソールアプリでログをファイルに出力するには?
組み込みのプロバイダーにファイル出力は無いため、Serilog や NLog などを追加します。書き込む側のコードはILogger<T>のままで変わりません。
AddHostedServiceで登録したサービスはSingletonですか?
はい。IHostedServiceのSingletonとして登録されます。コンストラクタで Scoped のサービスを受け取ると、Development環境ではコンパイル時ではなくホストのBuild()時(実行時)に例外になり、本番環境の既定設定では検出されません。IServiceScopeFactoryでスコープを作って解決してください。
AddScopedとAddSingletonはどう使い分けますか?
プロセス全体で共有して問題ないものはSingleton、1件の処理単位で状態を持つもの(DbContext など)はScopedです。ワーカーではScoped用のスコープを処理1回ごとに自分で作ります。
.NET Frameworkでも使えますか?
使えます。Microsoft.Extensions.Hosting 10.0.12 は .NET Framework 4.6.2 と .NET Standard 2.0 向けのビルドを含んでいます。.NET 10の新機能やサポート期限は.NET 10(LTS)とは?サポート期限・新機能・C# 14と.NET 8/9からの移行を解説を参照してください。