.NET Generic Host 是什麼 - DI、設定、日誌的基礎

· 更新日期: · · C#, .NET, Generic Host, Worker, 設計

更新紀錄(2 筆,最後更新 2026年09月04日)

本文的修改紀錄。已保存的更新前版本,可透過附有 DOI 的永久連結閱讀。

已將繁體中文版改寫為日文原文的完整翻譯。先前的繁體中文版只譯出日文原文的一部分,遺漏了章節、表格、Mermaid 圖、圖說與 FAQ。本次依日文原文將這些內容全部補回,並新增本文的知識地圖章節。技術主張與日文版一致。 查看更新前的版本 (DOI: 10.5281/zenodo.22279384)
補上了日文原文中已有的諮詢引導(consultation_services)。內文沒有改動。 查看更新前的版本 (DOI: 10.5281/zenodo.21616274)
初次發布
引用本文(DOI: 10.5281/zenodo.21616273)

本文保存於 Zenodo。以下同時提供一律指向最新版本的 DOI,以及固定於您正在閱讀版本的 DOI。

Go Komura(2026)。〈.NET Generic Host 是什麼 - DI、設定、日誌的基礎〉。小村軟體有限公司。https://doi.org/10.5281/zenodo.21616273 https://comcomponent.com/zh-TW/blog/2026/03/14/000-dotnet-generic-host-what-is/

DOI(最新版本)
10.5281/zenodo.21616273
DOI(此版本)
10.5281/zenodo.22297110

.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. 先講結論(一句話)
    • 1.1. 先把用語定下來
  2. 先看的整理表
    • 2.1. Generic Host 手上有哪些東西
    • 2.2. builder 的差別
    • 2.3. 為什麼入口有好幾個
  3. Generic Host 的全貌(圖)
  4. Generic Host 有什麼好處
    • 4.1. 能把啟動處理集中到一處
    • 4.2. DI / 設定 / 日誌從一開始就連在一起
    • 4.3. 正常結束與常駐運轉都比較好處理
  5. 最小組成
    • 5.1. 在主控台應用程式使用的最小範例
    • 5.2. appsettings.json
    • 5.3. 加上 BackgroundService
  6. 典型模式
    • 6.1. 短命的主控台工具
    • 6.2. worker / 背景服務
    • 6.3. ASP.NET Core 底下也有它
  7. 適合的情況
  8. 不適合 / 過剩的情況
  9. 常見陷阱
  10. 總結
  11. 參考資料

圖中實線表示始終成立的關係,虛線表示附帶條件的關係(成立條件寫在詳細頁面中各關係的說明中)。關係的完整清單(共 16 條,附依據與可信度)以及主要概念的定義,彙整在知識地圖詳細頁面(日文)。資料:JSON-LD / Turtle

1. 先講結論(一句話)

  • Generic Host 是把 .NET 應用程式的 啟動與有效期間 一併處理的基礎。
  • 其中包含 DI、設定、日誌、IHostedService / BackgroundService,以及應用程式的停止處理。
  • 新的非 Web 應用程式,先從 Host.CreateApplicationBuilder(args) 入手最直接了當。
  • ASP.NET Core 的 WebApplicationBuilder 也不是另一個世界,而是把同一套 host 想法擴展到 Web 用途的窗口。
  • 換句話說,Generic Host 講的不是單一 DI 容器,而是 把應用程式的組裝點與生命週期管理收攏在一起的機制

簡單說,應用程式一旦稍微超過「讀個引數、顯示一次就結束」的程度,Generic Host 就會相當管用。 反過來說,還沒長到那個程度的小工具,也不必每次都硬把它帶進來。

Generic Host 開始管用的範圍只顯示一次就結束的小工具不必每次都帶進來,稍微超過這個程度之後 Generic Host 就開始管用,說明這個判斷基準的圖。不必每次都帶進來相當管用顯示 1 次就結束的工具Generic Host稍微超過這個程度的應用程式

圖 1: 稍微超過「顯示一次就結束」的程度之後,Generic Host 就開始管用。

1.1. 先把用語定下來

本文接下來會用不少比喻,所以先把精確的說法擺在前面。

用語 精確地說 本文使用的比喻
DI(相依性注入 / Dependency Injection) 類別需要的對象不自己 new,而是由外部交進來的寫法。把要交出去的對象集中登記的置放處就是 DI 容器(IServiceProvider),Generic Host 一開始就帶著它 配線
Builder(HostApplicationBuilder 用來組裝 host 的物件。它有 ServicesConfigurationLogging 這些屬性,登記工作都寫在這裡。在呼叫 Build() 之前,應用程式不會動 組裝台
Host(IHost Build() 的結果,也就是 組裝完成的應用程式本體。它抱著 DI 容器、組態、日誌與 hosted service,從 Run() / RunAsync() 開始一路照顧到停止為止 基礎
Hosted service(IHostedService / BackgroundService 配合 host 的開始與停止而運作的處理容器。host 啟動後會呼叫 StartAsync,若是 BackgroundService 則會跑 ExecuteAsync 常駐的工作
Lifetime 從應用程式開始到停止為止的管理。接收 Ctrl+C、SIGTERM、服務停止之類的信號,把停止方式統一起來 生命週期

最容易混淆的是 Builder 和 Host。Builder 是組裝的那一側,Host 是組裝出來的結果Build() 就是兩者的分界。只要先掌握這一點,後面出現的「基礎」「盒子」「窗口」「入口」這些說法,讀起來就不會搞不清楚指的是哪一個。

Builder 與 Host 的分界Builder 是組裝的那一側,IHost 是組裝出來的結果,呼叫 Build 就是兩者分界的圖。Build()Builder(組裝的那一側)IHost(組裝出來的結果)Run / RunAsync 從開始照顧到停止

圖 2: Builder 是組裝的那一側,IHost 是組裝出來的結果,Build() 就是兩者的分界。

如果是第一次接觸 DI,這樣想就不會偏掉。與其自己寫出一連串的 new,不如在啟動時登記好「需要這個型別時,請把這個實作交過來」,接收的一側只要從建構函式的引數收下就行。登記的地方就是 builder.Services

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 包裝層」。 實際上,把它看成「把應用程式入口一帶整個配線起來的盒子」,最不容易偏掉。

2.2. builder 的差別

這裡也是先用一張表看完比較快。

入口 主要用途 寫起來的感覺 首選
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 用途的入口,這樣比較好梳理。

三個入口的關係CreateApplicationBuilder 與 CreateDefaultBuilder 具備相同的核心功能與預設動作,只是寫法流派不同,而 WebApplication.CreateBuilder 是把同一條路擴展到 Web 用途的入口的圖。擴展到 Web 用途的入口CreateApplicationBuilder相同的核心功能與預設動作CreateDefaultBuilder直接書寫的流派串接的流派WebApplication.CreateBuilder

圖 3: 兩個 builder 在相同的核心功能與預設動作之上只是寫法流派不同,Web 用途則是把它擴展開來的入口。

2.3. 為什麼入口有好幾個

入口之所以有好幾個,是因為 Web 那一側和非 Web 那一側各自長大之後才會合的來龍去脈。

  • 原本 ASP.NET Core 有 Web 專用的 Web Host(IWebHostBuilder),而給非 Web 應用程式用的 Generic Host(IHostBuilder)則是另外準備的。
  • 之後 ASP.NET Core 那一側收攏到 Generic Host,Web 和非 Web 都改為建立在同一套 host 想法之上。
  • 再之後,除了把回呼串起來的寫法(ConfigureServices 之類)以外,又多了直接寫到屬性的寫法(builder.Services 之類)的入口。Host.CreateApplicationBuilderWebApplication.CreateBuilder 屬於後者。

現在的官方文件把 Host.CreateApplicationBuilder 這一系列(IHostApplicationBuilder)整理成 給新專案用,也是現行範本的預設,把 Host.CreateDefaultBuilder 這一系列(IHostBuilder)整理成 為了與既有程式碼相容而保留下來的傳統做法。文件裡也明寫了兩者具備相同的核心功能與預設動作。

從 .NET Framework 或 .NET Core 3.1 時代的程式碼過來的人,會覺得「為什麼寫法有兩套」,但只要把它看成 並不是新舊兩種不同的東西並排,而是在會合的過程中入口變多了,應該就比較容易想通。只要沒有必須配合既有資產的理由,新專案用 Host.CreateApplicationBuilder 就可以。

入口有好幾個的來龍去脈Web 專用的 Web Host 與非 Web 用的 Generic Host 原本各自存在,ASP.NET Core 那一側收攏到 Generic Host 而會合,之後又多了直接寫到屬性的入口,說明這段來龍去脈的圖。Web Host(IWebHostBuilder)ASP.NET Core 收攏到 Generic HostGeneric Host(IHostBuilder)新增直接寫到屬性的入口CreateApplicationBuilder 與 WebApplication.CreateBuilder

圖 4: 各自長大的 Web Host 與 Generic Host 會合,過程中多了直接書寫風格的入口。

3. Generic Host 的全貌(圖)

把全貌粗略畫成圖,大致是這樣。

args / 環境變數 / appsettings.jsonHost.CreateApplicationBuilder(args)builder.Configurationbuilder.Servicesbuilder.LoggingIHostedService / BackgroundServicebuilder.Build()IHostRun / RunAsync開始、停止、Ctrl+C、SIGTERM

圖 5: 在 builder 登記組態、服務與日誌,用 Build() 取得的 IHost 交給 Run / RunAsync 跑起來,lifetime 就會一路連到 hosted service 的開始與停止。

一般會在 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 開始配線, 而是一開始就擺好了「一般使用起來相當夠用的基礎」。

一開始就載上的預設在 Host.CreateApplicationBuilder 的當下就已經載上主機組態、應用程式組態與預設日誌,一般使用起來夠用的基礎一開始就擺好了的圖。CreateApplicationBuilder(args)主機組態(DOTNET_ 開頭的環境變數與引數)應用程式組態(appsettings.json 等)預設的日誌(Console 等)一般使用起來夠用的基礎

圖 6: 建立 builder 的當下就已經載上組態與日誌的預設,並不是從 0 開始配線。

4. Generic Host 有什麼好處

4.1. 能把啟動處理集中到一處

Generic Host 最不起眼卻最有份量的效果,是應用程式的入口比較不會散開。

應用程式稍微長大之後,Main 附近會增加的大致是這些。

  • 設定檔的讀取
  • 各環境的抽換
  • 日誌記錄器的初始化
  • HttpClient、repository、service 的組裝
  • 背景處理的啟動
  • 收到結束信號時的善後

如果不用 host,全部自己手動接起來, 一開始還算輕鬆,之後入口就會愈來愈難收拾。

用了 Generic Host,Program.cs 就會明確成為「把相依性集中組裝起來的地方」。 光是這一點梳理,程式碼審查的容易程度就差很多。

啟動處理集中到一處不用 host 而全部手動接起來的話之後入口會愈來愈難收拾,用了 Generic Host 則 Program.cs 會明確成為把相依性集中組裝的地方的圖。不用 host,手動接起來之後入口會愈來愈難收拾使用 Generic HostProgram.cs 成為組裝的地方程式碼審查比較容易

圖 7: 手動配線會讓入口逐漸變得難以收拾,收攏到 host 之後 Program.cs 就成為明確的組裝地點。

4.2. DI / 設定 / 日誌從一開始就連在一起

用了 Generic Host,DI、設定、日誌從一開始就站在同一個基礎上。

例如在類別那一側,可以很自然地收下這些東西。

  • ILogger<T>
  • IConfiguration
  • IHostEnvironment
  • IOptions<T>

這裡管用的地方在於,設定的讀法和服務的建立方式比較不會變成兩套流派

設定只有一兩個的時候,直接讀 IConfiguration["Section:Key"] 也能動。 不過實務上設定一多,用 IOptions<T> 依 section 收攏成類別會比較安全。大致的參考值是鍵字串超過 5 個的時候。到了這個規模,打錯字會變成要到執行階段才發現的失敗,也很難追出哪個鍵是在哪裡被讀的。

同樣地,日誌與其到處手工建立 ILoggerFactory, 不如把 ILogger<T> 注入到需要的類別,更容易看清全貌。

Generic Host 方便的地方在於,它不會把這些當成各自獨立的事, 而是 當成整個應用程式的基礎一起處理

設定讀法的成長方式設定只有一兩個時直接讀 IConfiguration 也能動,但鍵字串超過 5 個之後改用 IOptions 依 section 收攏成類別會比較安全的圖。設定有 1〜2 個直接讀 IConfiguration鍵字串超過 5 個用 IOptions 收攏成類別打錯字要到執行階段才發現

圖 8: 設定還少的時候直接讀就夠,鍵超過 5 個之後用 IOptions 收攏比較安全。

4.3. 正常結束與常駐運轉都比較好處理

Generic Host 不只照顧「怎麼啟動」,也照顧「怎麼停下來」。

host 啟動後,會呼叫每個已登記的 IHostedServiceStartAsync。 在 worker 服務中,包含 BackgroundService 在內的 hosted service 會跑 ExecuteAsync

這裡說的「正常結束」,不是直接把處理切斷,而是照著

  • 送出停止的信號
  • 離開迴圈或等待
  • 收拾連線與資源

這個順序結束。

在長時間運轉的應用程式裡,這一點相當重要。 遇到 Ctrl+C、SIGTERM、服務停止之類的事件時,比較容易把整個應用程式的停止方式統一起來。

另外,想從應用程式這一側要求結束時,可以用 IHostApplicationLifetime.StopApplication()。 「工作已經做完了,請乾淨地退出」這個信號,可以在 host 的脈絡裡送出來。

正常結束的順序接到 Ctrl+C、SIGTERM 或服務停止的事件之後,送出停止的信號、離開迴圈或等待、收拾連線與資源,照這個順序結束的圖。StopApplication()Ctrl+C / SIGTERM / 服務停止送出停止的信號離開迴圈或等待收拾連線與資源來自應用程式端的結束要求

圖 9: 正常結束會照著停止信號、離開迴圈、善後的順序走,應用程式端則可以用 StopApplication() 對同一條流程送出信號。

5. 最小組成

5.1. 在主控台應用程式使用的最小範例

首先重要的是,使用 Generic Host 並不代表 一定要做出 BackgroundService

只跑一次的主控台工具, 只要想要 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;
    }
}

執行 dotnet run 之後,主控台會出現這樣的輸出(Message 的值來自下一節 5.2 要放的 appsettings.json)。

info: JobRunner[0]
      Environment: Production
info: JobRunner[0]
      Message: hello from Generic Host

info: 右邊的是日誌的分類(這裡是 ILogger<JobRunner>,所以是型別名稱)和事件識別碼。預設的主控台日誌記錄器就是用「第 1 行寫分類、第 2 行寫本文」的形式輸出。Environment 之所以是 Production,是因為環境變數 DOTNET_ENVIRONMENTASPNETCORE_ENVIRONMENT 都沒有設定時的預設值就是它。開發時想切換的話,設定 DOTNET_ENVIRONMENT=Development 再執行即可。

如果不會長時間常駐,不用走到 RunAsync() 也可以。 Build() 之後解析需要的服務,工作做完就直接結束。 這樣一樣能充分得到 Generic Host 的好處。

這一點意外地重要。 短命的工作,不必每次都把 Worker 範本帶進來。

短命工作的用法不會長時間常駐的話不用走到 RunAsync,用 Build 解析需要的服務、工作做完就直接結束的寫法一樣能得到 Generic Host 好處的圖。執行 Build()解析需要的服務做該做的工作直接結束不用走到 RunAsync()

圖 10: 短命的工作,只要 Build() 解析服務、執行完就結束,一樣能得到 host 的好處。

5.2. appsettings.json

以上面的範例來說,設定檔用這種最小形式就夠了。

{
  "Sample": {
    "Message": "hello from Generic Host"
  }
}

只有一個地方是常見的卡關處。在主控台專案裡,光是加入 appsettings.json 並不會複製到輸出資料夾。要在專案的屬性視窗把「複製到輸出目錄」設成「有更新時才複製」,或是在 csproj 裡寫上下面這段。

<ItemGroup>
  <Content Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

先知道忘記時的症狀,排查也會比較快。Generic Host 是把 appsettings.json 當成 可省略的檔案 來讀,所以沒有它也不會拋出例外,只是單純讀不到值而已。以上面的最小範例來說,會顯示 Message: (no message)。遇到「沒有出現錯誤,設定卻沒生效」時,請先查看輸出資料夾裡有沒有 appsettings.json

appsettings.json 不存在時的症狀即使 appsettings.json 沒有複製到輸出資料夾,因為是當成可省略的檔案來讀,所以不會拋出例外,只是單純讀不到值,說明這個症狀流程的圖。輸出資料夾裡沒有這個檔案當成可省略的檔案來讀不會拋出例外只是單純讀不到值先查看輸出資料夾

圖 11: 即使沒有 appsettings.json 也不會拋出例外,只是「讀不到值」,所以先查看輸出資料夾。

這個例子直接讀了原始的 configuration["Sample:Message"]。 只看一兩個值的話,這樣就夠了。

不過實務上設定變多之後,改成

  • 依 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 那一側才安全。

在 BackgroundService 使用 scoped 的寫法BackgroundService 本身沒有預設的 scope,所以注入 IServiceScopeFactory,在 ExecuteAsync 之中建立 scope,再於其中解析 job 那一側的服務才安全的圖。BackgroundService(沒有預設的 scope)注入 IServiceScopeFactory在 ExecuteAsync 內建立 scope在 scope 之中解析 job

圖 12: 沒有預設 scope 的 BackgroundService,要用 IServiceScopeFactory 建立 scope,再於其中解析 job。

另外,定期執行工具本身該怎麼選是另一個主題, 不過如果以 async 為基礎撰寫,PeriodicTimer 相當穩妥。 這一帶也和相關文章中談計時器的那一篇連得上。

6. 典型模式

6.1. 短命的主控台工具

像批次、轉換工具、維護命令這種 只做一次工作就結束的應用程式,一樣可以正常使用 Generic Host。

適合的是這些場面。

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

在這類應用程式裡,一開頭就搬進 BackgroundServiceRunAsync(), 除了稍嫌笨重之外,也等於過度使用了主機的生命週期管理。

短命的工作,就像前面的最小範例那樣,解析 JobRunner 並執行就夠了。

短命主控台工具的選法只做一次工作就結束的應用程式,搬進 BackgroundService 與 RunAsync 會過度使用生命週期管理,解析服務並執行就已經足夠的圖。足夠容易過剩只做一次工作就結束的應用程式解析 JobRunner 並執行BackgroundService 與 RunAsync()

圖 13: 對只跑一次的應用程式搬進 BackgroundService 是過剩的,解析服務並執行就夠了。

6.2. worker / 背景服務

常駐 worker、輪詢、佇列消費、監控、定期執行這類處理, 用 Generic Host 搭配 BackgroundService 相當直接了當。

特別好的是這幾點。

  • 啟動與停止的流程會在 host 那一側統一
  • 日誌、設定、DI 從一開始就能用
  • 容易在 Ctrl+C 或停止信號時把取消傳下去
  • 容易把常駐處理的本體從 Program.cs 分離出來

再者,也容易接上 Windows 服務或容器的脈絡。 如果要往常駐應用程式的方向培養,Generic Host 是相當自然的基礎。

要做成 Windows 服務時,與其以目前的工作目錄為前提去找檔案, 不如以 IHostEnvironment.ContentRootPath 為起點來思考,比較不容易出事。 因為「應用程式的基準路徑」是在 host 的脈絡裡決定的。

常駐 worker 的基礎常駐 worker 或定期執行用 Generic Host 搭配 BackgroundService 最直接了當,也容易接上 Windows 服務或容器脈絡的圖。常駐 worker / 定期執行Generic Host 與 BackgroundServiceWindows 服務容器常駐以 ContentRootPath 為起點尋找

圖 14: 常駐處理用 host 搭配 BackgroundService 最直接了當,也容易往 Windows 服務或容器發展。

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 也很管用。

Web 也在同一個 host 之上ASP.NET Core 應用程式也透過 WebApplication.CreateBuilder 建立在同一套 host 想法之上,HTTP 伺服器的啟動也包含在 host 的 lifetime 之中的圖。ASP.NET Core 應用程式WebApplication.CreateBuilder建立在同一套 host 想法之上HTTP 伺服器的啟動也在 lifetime 之中

圖 15: Web 那一側的 builder 也建立在同一套 host 想法之上,HTTP 伺服器的啟動也包含在 lifetime 裡。

7. 適合的情況

以下列出 Generic Host 容易用得順手的場面。

  • 會用到設定、日誌、DI 的主控台應用程式
  • queue consumer、poller、watchdog、scheduler 這類 worker
  • 想在 Ctrl+C 或 SIGTERM 時善後的長時間執行應用程式
  • 將來可能發展成 Windows 服務或容器常駐的應用程式
  • 想和 ASP.NET Core 用同一套擴充方法流派的應用程式

它們的共通點是, 「不想把應用程式的入口與生命週期管理做得草率」

話雖如此,光這樣還是不好劃線,所以也放一個判斷基準。下面幾項符合 2 項以上的話,一開始就放到 Generic Host 上,之後多半會比較輕鬆。

採用與否的判斷基準判斷基準的表符合 2 項以上就一開始放到 Generic Host 上,一項都不符合就可以視為不需要,說明這個判斷流程的圖。2 項以上一項都沒有數一數符合幾項判斷基準一開始就放到 Generic Host 上可以視為不需要 Generic Host

圖 16: 判斷基準符合 2 項以上就一開始放上去,一項都不符合就不必帶進來。

判斷基準 具體的界線
設定的數量 會依環境改變的設定有 3 個以上(連線目標、閾值、輸出目的地等)
日誌 需要留在檔案或事件記錄檔裡。不是寫到標準輸出就結束
執行的形式 會常駐。或是一天一次以上,以固定間隔運作
相依性 想從建構函式收下的對象有 3 個以上。有想在測試中抽換的對象
生命週期 Ctrl+C 或服務停止時,需要做到一半的善後
將來 有可能在 Windows 服務或容器上運作

8. 不適合 / 過剩的情況

反過來說,也有一開始不必讓 Generic Host 當主角的場面。

  • 只讀一次引數、輸出一次就結束的小工具
  • 只用幾十分鐘的隨手驗證程式碼
  • 函式庫專案
  • 只讀一個設定,還用不到 DI、日誌或生命週期管理的情況

在這些情況下,與其架起 host,不如直接寫在 Main 裡,要讀的量和檔案數都比較少。大致的參考值是,第 7 章的表一項都不符合的話,就可以認為不需要 Generic 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 服務裡以目前的工作目錄為前提
    • 檔案搜尋以 IHostEnvironment.ContentRootPath 為起點來思考會比較穩定。
  • 明明是短命的 CLI,一開始就用 BackgroundService 包起來
    • 只做一次的工作,解析一般的服務類別並執行就夠了。
  • BackgroundService 的定期執行裡隨手塞入 callback 計時器
    • 如果是用 async 的流程撰寫,PeriodicTimer 通常比較好讀,也比較不容易亂掉。

用 Generic Host 時, 只要 一開始就先分清楚「是短命的工作,還是常駐的工作」, 就會少掉很多猶豫。

一開始就要分清楚的問題一開始先分清楚是短命工作還是常駐工作,短命就只要解析一般的服務類別並執行,常駐則使用 BackgroundService 與 host 的 lifetime 管理,說明這個梳理方式的圖。短命常駐是短命工作,還是常駐工作只要解析服務並執行BackgroundService 與 lifetime 管理

圖 17: 一開始就分清楚短命還是常駐,要用到 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. 參考資料

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

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

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

常見問題

整理諮詢這個主題時常見的問題。

Generic Host 是什麼?
它是把 .NET 應用程式的啟動與有效期間一併處理的基礎,其中包含 DI、設定(Configuration)、日誌、IHostedService / BackgroundService,以及應用程式的停止處理。與其把它看成單純的 DI 容器包裝層,不如理解成把應用程式的組裝點與生命週期管理收攏在一起的機制,比較不會偏掉。只要設定、日誌、相依性、啟動、結束稍微增加,它就能發揮效果。
Host.CreateApplicationBuilder 和 Host.CreateDefaultBuilder 該用哪一個?
新的非 Web 應用程式,從 Host.CreateApplicationBuilder(args) 入手最直接了當。兩者具備相同的核心功能與預設動作,並不是一邊是新功能、一邊是完全不同的東西。差別主要在寫法的流派:CreateApplicationBuilder 是直接寫到 builder.Services 之類屬性的風格,CreateDefaultBuilder 則是把 ConfigureServices 之類串接起來的風格。若有必須配合既有程式碼或以舊擴充方法為主的架構的理由,才選 CreateDefaultBuilder。
在主控台應用程式使用 Generic Host 有價值嗎?
只要想要 DI、設定、日誌,即使是只跑一次的主控台工具也很夠用。不一定要做出 BackgroundService,用 Build() 之後解析需要的服務、工作做完就直接結束的寫法,一樣能得到 Generic Host 的好處。反過來說,對於只讀一次引數、輸出一次就結束的小工具,或是隨手寫的驗證程式碼,它就過剩了,並不是每次都得帶進來的東西。
在 BackgroundService 中要怎麼使用 scoped 服務?
BackgroundService 沒有預設的 scope,因此直接用建構函式注入 scoped 服務並不安全。比較安全的做法是注入 IServiceScopeFactory,在 ExecuteAsync 之中明確建立 scope,再於其中解析 job 那一側的服務。想使用 DbContext 這類 scoped 服務時,尤其需要留意這個寫法。

作者檔案

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

Go Komura

小村軟體有限公司 代表

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

回到部落格一覽