.NET 的 Generic Host 是什麼 - 先整理 DI、設定、日誌、BackgroundService

· · C#, .NET, Generic Host, Worker, 設計

.NET 開始寫主控台應用程式或 worker,最初只要在 Main 寫點處理就行。 但稍微長大,這一帶會增加。

  • 想讀 appsettings.json
  • 想用環境變數覆寫
  • 想用 ILogger 輸出日誌
  • 不想讓服務的建立充滿 new
  • 想在背景跑迴圈
  • 想用 Ctrl+C 或服務停止乾淨結束

這時出現的是 Generic Host。 但這名稱也容易混淆。

  • Host.CreateApplicationBuilderHost.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 透過 IHostApplicationLifetimeIHostEnvironment 等處理開始・停止 容易對齊 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 就這個

CreateApplicationBuilderCreateDefaultBuilder 不是一個是新功能一個是別物的話題。

兩者都有相同的核心功能和預設動作。 不同的主要是 寫法流派

新的非 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.Configurationbuilder.Logging, 最後 Build() 得到 IHost,用 Run() / RunAsync() 跑。

樸素但大的是 Host.CreateApplicationBuilder(args) 的時點已載入相當多東西。 預設例如放入以下。

  • 內容根為當前目錄
  • 主機配置是有 DOTNET_ 前綴的環境變數與命令列引數
  • 應用程式配置是 appsettings.jsonappsettings.{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>
  • IConfiguration
  • IHostEnvironment
  • IOptions<T>

這裡有效的是 設定的讀法與服務的造法不易變成不同流派

設定只有 1、2 個,直接讀 IConfiguration["Section:Key"] 就動。 但實務上設定增加,用 IOptions<T> 每個 section 綁到類別較穩妥。

同樣地日誌與其到處手建 ILoggerFactory,不如把 ILogger<T> 注入到必要的類別更透視。

Generic Host 方便的是能把這些不當作不同的話,而是 作為應用程式整體的基礎一起處理

4.3. 容易處理正常結束與常駐運行

Generic Host 不只照料「怎麼啟動」,也照料「怎麼停止」。

host 啟動後,各登錄的 IHostedServiceStartAsync 會被呼叫。 在 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 點。

  1. BackgroundService 的本體是 ExecuteAsync
  2. 想要 scoped 的相依,用 IServiceScopeFactory 建 scope

BackgroundService 本身沒預設 scope。 例如想用 DbContext 這類 scoped 服務, 如上在 scope 中解析 job 側才安全。

另外定期執行工具本身的選擇是另一個主題, 如果用 async 為基礎寫,PeriodicTimer 相當穩妥。 這一帶也與相關的計時器文章相連。

6. 典型模式

6.1. 短命的主控台工具

批次、轉換工具、維護命令等, 做 1 次工作就結束的應用程式,Generic Host 也普通可用。

適合的例如以下場面。

  • 想讀設定檔
  • 想輸出日誌
  • 想注入 HttpClient 或 repository
  • 想回傳結束碼

這類應用程式,直接帶 BackgroundServiceRunAsync() 會稍重,且過度使用主機壽命管理。

短命工作就像前面最小例,解決 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.Services
  • builder.Configuration
  • builder.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 較安全。
  • 只跑一次的 worker 卻沒告訴 host 要停
    • Worker 範本做「run once」時,工作結束時要呼 IHostApplicationLifetime.StopApplication(),否則 host 會繼續跑。
  • 想正常結束卻用 Environment.Exit 切斷
    • 用 host 時想乾淨停止的場面 StopApplication() 比較有道理。
  • Windows Service 以 current directory 為前提
    • 檔案搜尋以 IHostEnvironment.ContentRootPath 為起點思考比較穩定。
  • 短命 CLI 卻從一開始用 BackgroundService 包住
    • 只做一次的工作,解析一般服務類別執行就夠。
  • BackgroundService 的定期執行粗略地塞入 callback 計時器
    • async 的流程寫的話 PeriodicTimer 比較好讀、比較不易亂。

Generic Host 中, 最先分開「是短命工作還是常駐工作」 就會相當不易迷惘。

10. 總結

用一句話說 Generic Host 就是 一併處理 .NET 應用程式入口與壽命管理的基礎

再整理要點如下。

  1. Generic Host 不只 DI,包含設定、日誌、停止處理、hosted service
  2. 新的非 Web 應用程式,先用 Host.CreateApplicationBuilder(args) 自然
  3. 短命工作,不用 BackgroundService,build 後執行也可以
  4. 常駐處理,BackgroundService 和 host 的 lifetime 管理相當有效
  5. BackgroundService 沒預設 scope,scoped 服務要明確建 scope
  6. ASP.NET Core 的 WebApplicationBuilder 思想上也在同流程上

Generic Host 不是為儀式而存在的工具。 設定、日誌、相依、啟動、結束稍微增加就都集中到入口的工具,避免它們躲進牆內。

反之還不到那程度的小工具,不用帶進來。 能辨識這點時 Generic Host 就不是「不自覺放入的東西」,而是用處清楚的實務基礎。

11. 參考資料

共用相同標籤的最新文章。能以相近的主題延伸理解。

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

本文連結到以下服務頁面,歡迎從最接近的入口查看。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽