.NET 的 Generic Host 是什麼 - 先整理 DI、設定、日誌、BackgroundService
· 小村 豪 · C#, .NET, Generic Host, Worker, 設計
在 .NET 開始寫主控台應用程式或 worker,最初只要在 Main 寫點處理就行。
但稍微長大,這一帶會增加。
- 想讀
appsettings.json - 想用環境變數覆寫
- 想用
ILogger輸出日誌 - 不想讓服務的建立充滿
new - 想在背景跑迴圈
- 想用
Ctrl+C或服務停止乾淨結束
這時出現的是 Generic Host。
但這名稱也容易混淆。
Host.CreateApplicationBuilder和Host.CreateDefaultBuilder有什麼差別IHost和 DI 容器相同嗎- 與
BackgroundService怎麼連起來 - 和 ASP.NET Core 的
WebApplicationBuilder是不同東西嗎 - 在主控台應用程式也有使用價值嗎
這裡混在一起,Generic Host 會看起來「像 Web 專用的東西」或反過來「什麼都該做成 host」。兩者都稍微粗略。
本文以 .NET 6 以後的現在實務感為前提,先整理以下 4 個。
- Generic Host 的真面目
- 一併照料什麼
Host.CreateApplicationBuilder/Host.CreateDefaultBuilder/WebApplication.CreateBuilder的關係- 從哪進入會穩妥
1. 先講結論(一句話)
- Generic Host 是把 .NET 應用程式的 啟動與有效期間 一併處理的基礎。
- 其中包含 DI、設定、日誌、
IHostedService/BackgroundService、應用程式的停止處理。 - 新的非 Web 應用程式,先從
Host.CreateApplicationBuilder(args)進入較自然。 - ASP.NET Core 的
WebApplicationBuilder也不是別的世界,是把相同 host 思考方式擴展到 Web 的入口。 - 也就是說 Generic Host 不是單純的 DI 容器的話,而是 把應用程式的組裝點與壽命管理一起處理的機制。
簡言之,應用程式稍微超過「讀取引數顯示 1 次結束」時,Generic Host 相當有效。 反過來還沒長到那程度的小工具,未必每次都要帶進來。
2. 先看的整理表
2.1. Generic Host 抱的東西
最初把這個盒子的內容分開會相當輕鬆。
| 要素 | Generic Host 照料什麼 | 有什麼好 |
|---|---|---|
| DI | 從 IServiceCollection 組裝服務 |
容易減少 new 的連鎖 |
| Configuration | 彙總 appsettings.json、環境變數、命令列引數等 |
容易處理環境別差 |
| Logging | 建立使用 ILogger<T> 的基礎 |
之後容易替換日誌輸出 |
| Hosted service | 處理 IHostedService / BackgroundService 的啟動與停止 |
容易把常駐處理與應用程式本體分離 |
| Lifetime | 透過 IHostApplicationLifetime、IHostEnvironment 等處理開始・停止 |
容易對齊 Ctrl+C、SIGTERM、服務停止等結束方式 |
這裡重要的是 Generic Host 不是「方便的 DI 封裝 1 個」。 實際上看作把應用程式入口周邊一併配線的盒子最不偏離。
2.2. builder 的差別
這裡也一開始以 1 頁看較快。
| 入口 | 主要用途 | 書寫感 | 先選 |
|---|---|---|---|
Host.CreateApplicationBuilder(args) |
主控台 / worker 等新的非 Web 應用程式 | 直接寫到 builder.Services / builder.Configuration / builder.Logging |
新規就這個 |
Host.CreateDefaultBuilder(args) |
既有程式碼或以舊擴充方法為主的構成 | 鏈接 ConfigureServices 等 |
有既有資產就這個 |
WebApplication.CreateBuilder(args) |
ASP.NET Core Web 應用程式 / API | 在 Generic Host 加入 Web 用的便利的入口 | Web 就這個 |
CreateApplicationBuilder 和 CreateDefaultBuilder 不是一個是新功能一個是別物的話題。
兩者都有相同的核心功能和預設動作。 不同的主要是 寫法流派。
新的非 Web 應用程式現在從 Host.CreateApplicationBuilder(args) 進入較自然。
WebApplication.CreateBuilder(args) 看作把那流程擴展到 Web 的入口比較好整理。
3. Generic Host 的整體圖
粗略把整體圖畫成圖如下。
flowchart LR
Args["args / 環境變數 / appsettings.json"] --> Builder["Host.CreateApplicationBuilder(args)"]
Builder --> Config["builder.Configuration"]
Builder --> Services["builder.Services"]
Builder --> Logging["builder.Logging"]
Services --> Hosted["IHostedService / BackgroundService"]
Builder --> Build["builder.Build()"]
Build --> Host["IHost"]
Host --> Run["Run / RunAsync"]
Run --> Lifetime["開始・停止・Ctrl+C・SIGTERM"]
Lifetime --> Hosted
通常在 Program.cs 建 builder,
往 builder.Services 加服務,
依需要調整 builder.Configuration 或 builder.Logging,
最後 Build() 得到 IHost,用 Run() / RunAsync() 跑。
樸素但大的是 Host.CreateApplicationBuilder(args) 的時點已載入相當多東西。
預設例如放入以下。
- 內容根為當前目錄
- 主機配置是有
DOTNET_前綴的環境變數與命令列引數 - 應用程式配置是
appsettings.json、appsettings.{Environment}.json、Development 的 user secrets、環境變數、命令列引數 - 日誌是 Console / Debug / EventSource / EventLog(僅 Windows)
- 在
Development環境做 scope 驗證與相依驗證
也就是不是從 0 無腦配線,而是從一開始放好了「普通使用範圍相當足夠的基礎」。
4. Generic Host 有什麼好
4.1. 能把啟動處理集中到一處
Generic Host 最樸素且大的效果是應用程式入口不易散亂。
應用程式稍微長大,Main 周圍會增加以下。
- 設定檔的讀取
- 環境別的替換
- 記錄器的初始化
HttpClient或 repository 或 service 的組裝- 背景處理的啟動
- 終止訊號時的後續處理
沒有 host 全用手串起來,最初輕但之後入口會變黏。
用 Generic Host,Program.cs 會清楚成為「一併組裝相依關係的地方」。
光這個整理程式碼審查就容易很多。
4.2. DI / 設定 / 日誌從一開始就連上
用 Generic Host,DI、設定、日誌從一開始就乘上同一基礎。
例如類別端能普通地接收:
ILogger<T>IConfigurationIHostEnvironmentIOptions<T>
這裡有效的是 設定的讀法與服務的造法不易變成不同流派。
設定只有 1、2 個,直接讀 IConfiguration["Section:Key"] 就動。
但實務上設定增加,用 IOptions<T> 每個 section 綁到類別較穩妥。
同樣地日誌與其到處手建 ILoggerFactory,不如把 ILogger<T> 注入到必要的類別更透視。
Generic Host 方便的是能把這些不當作不同的話,而是 作為應用程式整體的基礎一起處理。
4.3. 容易處理正常結束與常駐運行
Generic Host 不只照料「怎麼啟動」,也照料「怎麼停止」。
host 啟動後,各登錄的 IHostedService 的 StartAsync 會被呼叫。
在 worker 服務中,包含 BackgroundService 的 hosted service 的 ExecuteAsync 會跑。
這裡的「正常結束」不是突然切處理,而是
- 放出停止的訊號
- 跳出迴圈或等待
- 整理連線或資源
踩這些順序結束。
在長時間動作的應用程式中這相當重要。
Ctrl+C、SIGTERM、服務停止這類事件中,容易對齊整個應用程式的停止方式。
另外想從應用程式端要求結束時,能用 IHostApplicationLifetime.StopApplication()。
以 host 的脈絡放出「工作結束了,請乾淨地關閉」的訊號。
5. 最小構成
5.1. 主控台應用程式的最小使用例
先重要的是,使用 Generic Host 不代表一定要做 BackgroundService。
只跑 1 次的主控台工具,如果想要 DI、設定、日誌,Generic Host 也足夠可用。
普通的 console 專案之後要載入,先參照 Microsoft.Extensions.Hosting。
dotnet add package Microsoft.Extensions.Hosting
Program.cs 的最小例如下。
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddSingleton<JobRunner>();
using IHost host = builder.Build();
try
{
JobRunner runner = host.Services.GetRequiredService<JobRunner>();
await runner.RunAsync();
return 0;
}
catch (Exception ex)
{
ILogger logger = host.Services
.GetRequiredService<ILoggerFactory>()
.CreateLogger("Program");
logger.LogError(ex, "Unhandled exception occurred during job execution.");
return 1;
}
internal sealed class JobRunner(
ILogger<JobRunner> logger,
IConfiguration configuration,
IHostEnvironment hostEnvironment)
{
public Task RunAsync()
{
string message = configuration["Sample:Message"] ?? "(no message)";
logger.LogInformation("Environment: {EnvironmentName}", hostEnvironment.EnvironmentName);
logger.LogInformation("Message: {Message}", message);
return Task.CompletedTask;
}
}
不長時間常駐的話,不必到 RunAsync()。
Build() 解決必要服務,工作完成就結束。
這樣 Generic Host 的好處也足夠可用。
這裡意外重要。 不需要對短命工作每次都帶 Worker 範本。
5.2. appsettings.json
上例中設定檔這種最小形式就夠。
{
"Sample": {
"Message": "hello from Generic Host"
}
}
此例生讀 configuration["Sample:Message"]。
只看 1 或 2 個值就這樣足夠。
但實務上設定增加時,
- 每個 section 分到類別
- 用
IOptions<T>注入 - 啟動時驗證
這樣靠過去,可避免鍵字串散佈。
另外 Generic Host 的預設除 appsettings.json 外,
appsettings.{Environment}.json、環境變數、命令列引數也都連上,
「只在開發時替換」「在正式用環境變數覆寫」會相當自然。
5.3. 載入 BackgroundService
長時間動作的處理用 BackgroundService 相當自然。
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddScoped<PollingJob>();
builder.Services.AddHostedService<PollingWorker>();
using IHost host = builder.Build();
await host.RunAsync();
internal sealed class PollingWorker(
IServiceScopeFactory scopeFactory,
ILogger<PollingWorker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using PeriodicTimer timer = new(TimeSpan.FromSeconds(30));
while (await timer.WaitForNextTickAsync(stoppingToken))
{
using IServiceScope scope = scopeFactory.CreateScope();
PollingJob job = scope.ServiceProvider.GetRequiredService<PollingJob>();
await job.RunAsync(stoppingToken);
logger.LogInformation("Polling completed.");
}
}
}
internal sealed class PollingJob(ILogger<PollingJob> logger)
{
public Task RunAsync(CancellationToken cancellationToken)
{
logger.LogInformation("Do work here.");
return Task.CompletedTask;
}
}
此例想看的有 2 點。
BackgroundService的本體是ExecuteAsync- 想要 scoped 的相依,用
IServiceScopeFactory建 scope
BackgroundService 本身沒預設 scope。
例如想用 DbContext 這類 scoped 服務,
如上在 scope 中解析 job 側才安全。
另外定期執行工具本身的選擇是另一個主題,
如果用 async 為基礎寫,PeriodicTimer 相當穩妥。
這一帶也與相關的計時器文章相連。
6. 典型模式
6.1. 短命的主控台工具
批次、轉換工具、維護命令等, 做 1 次工作就結束的應用程式,Generic Host 也普通可用。
適合的例如以下場面。
- 想讀設定檔
- 想輸出日誌
- 想注入
HttpClient或 repository - 想回傳結束碼
這類應用程式,直接帶 BackgroundService 和 RunAsync() 會稍重,且過度使用主機壽命管理。
短命工作就像前面最小例,解決 JobRunner 執行就夠。
6.2. worker / 背景服務
常駐 worker、輪詢、佇列消費、監視、定期執行這類處理中,
Generic Host 和 BackgroundService 的組合相當自然。
特別好的是以下點。
- 啟動與停止的流程在 host 側對齊
- 日誌、設定、DI 從一開始可用
- 容易用
Ctrl+C或停止訊號流出取消 - 容易把常駐處理本體從
Program.cs分離
此外也容易與 Windows Service 或容器的脈絡連結。 要當常駐應用程式養,Generic Host 是相當自然的基礎。
做 Windows Service 時,比起以當前目錄為前提找檔案,
以 IHostEnvironment.ContentRootPath 為起點思考事故較少。
因為 host 的脈絡決定「應用程式的基準路徑」。
6.3. ASP.NET Core 下也有它
Web 應用程式 / API 中用 WebApplication.CreateBuilder(args),
乍看可能與 Generic Host 像別世界。
但感覺上相當連結。
builder.Servicesbuilder.Configurationbuilder.Logging
書寫感相似是那個原因。
ASP.NET Core 中 HTTP 伺服器的啟動也進入 host 的 lifetime 中。
也就是讀 Web 側 Program.cs 時「為何在這裡碰 DI、設定、日誌」容易懂,這意義上理解 Generic Host 也有效。
7. 適合的情況
Generic Host 容易舒服對上的例如以下。
- 使用設定、日誌、DI 的主控台應用程式
- 像 queue consumer、poller、watchdog、scheduler 這類的 worker
- 想用
Ctrl+C或 SIGTERM 善後的長時間執行應用程式 - 未來可能長成 Windows Service / 容器常駐的應用程式
- 想對齊與 ASP.NET Core 相同擴充群流派的應用程式
共通的是 「不想粗略地處理應用程式的入口與壽命管理」。
8. 不適合 / 過剩的情況
反過來,從一開始不用把 Generic Host 作主角的場面也有。
- 讀一次引數輸出一次就結束的小工具
- 只用幾十分鐘的粗略驗證程式碼
- 函式庫專案
- 只讀一個設定、不需要 DI 或日誌或壽命管理的情況
這裡比起立 host,單純的實作更安靜。
重要的是 Generic Host 強不代表對所有 executable 必需。
9. 容易踩的陷阱
最後整理 Generic Host 起手容易踩的點。
- 把 Generic Host 只當 DI 容器看
- 實際上是含啟動・停止・設定・日誌・hosted service 的基礎。
- 新規應用程式卻慣性從
Host.CreateDefaultBuilder開始- 若沒有配合既有程式碼的理由,先用
Host.CreateApplicationBuilder比較自然。
- 若沒有配合既有程式碼的理由,先用
- 把 scoped 服務直接塞進
BackgroundService- hosted service 沒預設 scope。用
IServiceScopeFactory建 scope 較安全。
- hosted service 沒預設 scope。用
- 只跑一次的 worker 卻沒告訴 host 要停
- Worker 範本做「run once」時,工作結束時要呼
IHostApplicationLifetime.StopApplication(),否則 host 會繼續跑。
- Worker 範本做「run once」時,工作結束時要呼
- 想正常結束卻用
Environment.Exit切斷- 用 host 時想乾淨停止的場面
StopApplication()比較有道理。
- 用 host 時想乾淨停止的場面
- Windows Service 以 current directory 為前提
- 檔案搜尋以
IHostEnvironment.ContentRootPath為起點思考比較穩定。
- 檔案搜尋以
- 短命 CLI 卻從一開始用
BackgroundService包住- 只做一次的工作,解析一般服務類別執行就夠。
- 在
BackgroundService的定期執行粗略地塞入 callback 計時器- 用
async的流程寫的話PeriodicTimer比較好讀、比較不易亂。
- 用
Generic Host 中, 最先分開「是短命工作還是常駐工作」 就會相當不易迷惘。
10. 總結
用一句話說 Generic Host 就是 一併處理 .NET 應用程式入口與壽命管理的基礎。
再整理要點如下。
- Generic Host 不只 DI,包含設定、日誌、停止處理、hosted service
- 新的非 Web 應用程式,先用
Host.CreateApplicationBuilder(args)自然 - 短命工作,不用
BackgroundService,build 後執行也可以 - 常駐處理,
BackgroundService和 host 的 lifetime 管理相當有效 BackgroundService沒預設 scope,scoped 服務要明確建 scope- ASP.NET Core 的
WebApplicationBuilder思想上也在同流程上
Generic Host 不是為儀式而存在的工具。 設定、日誌、相依、啟動、結束稍微增加就都集中到入口的工具,避免它們躲進牆內。
反之還不到那程度的小工具,不用帶進來。 能辨識這點時 Generic Host 就不是「不自覺放入的東西」,而是用處清楚的實務基礎。
11. 參考資料
- .NET Generic Host - .NET
- Worker Services in .NET
- Use scoped services within a BackgroundService - .NET
- Configuration in .NET
- Options pattern in .NET
- .NET Generic Host in ASP.NET Core
- Create Windows Service using BackgroundService - .NET
- 相關文章:PeriodicTimer / System.Threading.Timer / DispatcherTimer 的區分使用 - 先整理 .NET 的定期執行
- 相關文章:C# async/await 的最佳實踐 - Task.Run 與 ConfigureAwait 的判斷表
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
把 Generic Host / BackgroundService 帶進桌面應用程式的理由 - 啟動・壽命・graceful shutdown 的整理會輕鬆很多
整理把 .NET Generic Host 與 BackgroundService 帶進 WPF / WinForms 桌面應用程式的理由,把啟動、lifetime、graceful shutdown 集中於入口管理。透過 StartAsync / ExecuteAsync...
.NET 的 Native AOT 是什麼 - 先釐清與 JIT、ReadyToRun、trimming 的差異
把 .NET 的 Native AOT 與 JIT、ReadyToRun、self-contained、single-file、trimming、source generator 放在一起釐清,並從啟動、發布、相依性的角度整理它合適與不合適的情境,幫助讀者判斷該不該採用。
PeriodicTimer / System.Threading.Timer / DispatcherTimer 的區分使用 - 先整理 .NET 的定期執行
整理 .NET 中 PeriodicTimer、System.Threading.Timer、DispatcherTimer 的差異與使用場景,從執行緒、async 流程、callback 重疊三個角度切入,協助你在 worker、ThreadPool 背景處理及 WPF ...
FileSystemWatcher 的使用方法與注意事項 - 漏掉、重複通知、完成判定的陷阱
本文整理 FileSystemWatcher 的正確用法。把事件視為跡象而非完成通知,將通知摺疊為重新掃描請求,由傳送端以 temp 後 rename 明示完成,多 worker 以原子性 claim 取得所有權,最後以 full rescan 與 idempotency ...
C# async/await 的最佳實踐 - Task.Run 與 ConfigureAwait 的判斷表
本文以 .NET 6 之後的一般 C# 開發為前提,將 async/await 周邊的判斷整理成可立即查的對照表。先依 I/O 等待與 CPU 計算分流,再決定 Task.Run、Task.WhenAll、Parallel.ForEachAsync、Channel、Peri...
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
Generic Host & 應用程式架構
整理 Generic Host、BackgroundService、DI、組態、日誌以及應用程式生命週期設計的主題頁面。
與本主題相關的服務
本文連結到以下服務頁面,歡迎從最接近的入口查看。
Windows 應用程式開發
支援包含常駐處理、設備連動、運作日誌與可維護結構的 Windows 桌面應用程式。
技術諮詢 & 設計審查
協助釐清設計方向、架構邊界、生命週期責任,以及既有 Windows 資產的處理方式。
作者檔案
本文作者的個人檔案頁面。
Go Komura
小村軟體有限公司 代表
以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。