.NET Generic Host 是什么 - DI、配置、日志的基础

· 更新日期: · · C#, .NET, Generic Host, Worker, 设计

更新记录(2 条,最后更新 2026年09月03日)

本文的修改记录。已保存的更新前版本,可通过带有 DOI 的永久链接阅读。

本文此前是日文原文的节译,缺少大量章节、表格、Mermaid 图、图题、脚注与 FAQ。现已改写为日文原文的完整译文,技术主张与日文版一致,并补上了此前缺失的图与表,同时统一了全篇的术语译法。 查看更新前的版本 (DOI: 10.5281/zenodo.22276774)
补充了日文原文中已有的咨询引导(consultation_services)。正文内容没有改动。 查看更新前的版本 (DOI: 10.5281/zenodo.21615455)
首次发布
引用本文(DOI: 10.5281/zenodo.21615454)

本文保存于 Zenodo。以下同时提供始终指向最新版本的 DOI,以及固定于您正在阅读版本的 DOI。

小村 豪(2026)。《.NET Generic Host 是什么 - DI、配置、日志的基础》。小村软件有限公司。https://doi.org/10.5281/zenodo.21615454 https://comcomponent.com/zh-CN/blog/2026/03/14/000-dotnet-generic-host-what-is/

DOI(最新版本)
10.5281/zenodo.21615454
DOI(此版本)
10.5281/zenodo.22281971

开始用 .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 及以后版本的实务感受为前提,先整理清楚下面这四点。

  • 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 就开始发挥作用。不必每次都引入会相当有效输出一次就结束的工具Generic Host稍微超出这个范围的应用

图1:一旦超出“输出一次就结束”一点点,Generic Host 就开始发挥作用。

1.1. 先把术语定下来

本文后面会用到不少比喻,所以先把准确的说法放在这里。

术语 准确地说 本文使用的比喻
DI(依赖注入 / Dependency Injection) 一种写法:类所需要的对象不由自己 new,而是从外部传进来。把这些待传入的对象集中登记起来的地方就是 DI 容器(IServiceProvider),Generic Host 从一开始就带着它 配线
Builder(HostApplicationBuilder) 用来组装 host 的对象。它带有 Services、Configuration、Logging 这些属性,注册工作就写在这里。在调用 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 通过 IHostApplicationLifetime、IHostEnvironment 等处理启动与停止 便于统一 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 应用首选

CreateApplicationBuilder 和 CreateDefaultBuilder, 并不是一方是新功能、另一方是完全不同的东西这样的关系。

两者拥有相同的核心功能和默认行为。 不同的地方主要在于写法风格。

对于新的非 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.CreateApplicationBuilder 和 WebApplication.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 Host 合流Generic 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.Configuration 或 builder.Logging, 最后调用 Build() 得到 IHost,再用 Run() / RunAsync() 让它运行起来。

一个不太起眼但影响很大的地方是,在 Host.CreateApplicationBuilder(args) 这一步,其实已经加载了相当多的内容。 默认情况下,例如会包含以下这些:

  • 内容根目录是当前目录
  • host 配置来自带 DOTNET_ 前缀的环境变量和命令行参数
  • 应用配置来自 appsettings.json、appsettings.{Environment}.json、Development 环境下的 user secrets、环境变量、命令行参数
  • 日志输出到 Console / Debug / EventSource / EventLog(仅 Windows)
  • 在 Development 环境下会进行 scope 验证和依赖关系验证

也就是说,并不是从零开始手动搭建所有配线, 而是一开始就放置好了一个“日常使用基本足够”的基础。

一开始就已经加载好的默认项说明在 Host.CreateApplicationBuilder 这一步,host 配置、应用配置、默认日志等就已经加载好,日常使用基本足够的基础从一开始就放在那里。CreateApplicationBuilder(args)host 配置(DOTNET_ 系与命令行参数)应用配置(appsettings.json 等)默认日志(Console 等)日常使用基本足够的基础

图6:在创建 builder 的那一刻,配置与日志的默认项就已经加载好,并不是从零开始配线。

4. Generic Host 好在哪里

4.1. 可以把启动处理集中到一处

Generic Host 最不起眼却影响最大的好处,就是应用的入口不容易变得分散。

随着应用的发展,Main 周围通常会逐渐增加以下内容。

  • 读取配置文件
  • 按环境替换配置
  • 初始化 logger
  • 组装 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 启动后,注册的各个 IHostedService 的 StartAsync 就会被调用。 在 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 也完全可以使用。

如果要在普通的控制台项目中后续加入,首先需要引用 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>,所以显示的是类型名)和事件 ID。默认的控制台日志记录器就是按照“第一行是类别,第二行是正文”这种形式输出的。Environment 之所以是 Production,是因为在既没有设置环境变量 DOTNET_ENVIRONMENT 也没有设置 ASPNETCORE_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;
    }
}

这个示例中要关注两点。

  1. BackgroundService 的主体是 ExecuteAsync
  2. 如果需要 scoped 依赖关系,用 IServiceScopeFactory 来创建 scope

BackgroundService 本身没有默认的 scope。 比如想使用像 DbContext 这样的 scoped 服务时, 像上面这样在 scope 内解析任务一侧的服务会更安全。

在 BackgroundService 中使用 scoped 的写法说明由于 BackgroundService 本身没有默认的 scope,注入 IServiceScopeFactory 并在 ExecuteAsync 中创建 scope、再在其中解析任务一侧服务的写法更安全。BackgroundService(没有默认 scope)注入 IServiceScopeFactory在 ExecuteAsync 中创建 scope在 scope 中解析 job

图12:在没有默认 scope 的 BackgroundService 中,用 IServiceScopeFactory 创建 scope,并在其中解析 job。

另外,定期执行本身工具的选择是另一个话题, 如果以 async 为基础编写,PeriodicTimer 会相当稳妥。 这一点也与相关文章中的定时器话题相连。

6. 典型模式

6.1. 短命的控制台工具

像批处理、转换工具、维护命令这类只执行一次工作就结束的应用, 同样可以正常使用 Generic Host。

适合的场景包括:

  • 想读取配置文件
  • 想输出日志
  • 想注入 HttpClient 或 repository
  • 想返回结束代码

对于这类应用,如果一开始就引入 BackgroundService 和 RunAsync(), 会显得有些沉重,也会过度使用 host 的生命周期管理。

对于短命的任务,像前面的最小示例那样解析并执行 JobRunner 就足够了。

短命控制台工具的选择方式说明对于只执行一次工作就结束的应用,引入 BackgroundService 和 RunAsync 会过度使用生命周期管理,只要解析并执行服务就足够了。足够容易显得过度只执行一次工作就结束的应用解析并执行 JobRunnerBackgroundService 与 RunAsync()

图13:对只跑一次的应用引入 BackgroundService 属于过度设计,解析并执行服务就够了。

6.2. worker / 后台服务

对于常驻 worker、轮询、队列消费、监控、定期执行这类处理, Generic Host 与 BackgroundService 的组合会相当自然。

尤其令人满意的地方包括:

  • 启动与停止的流程在 host 一侧统一
  • 日志、配置、DI 从一开始就可以使用
  • 便于通过 Ctrl+C 或停止信号传递取消操作
  • 便于把常驻处理的主体从 Program.cs 中分离出来

而且,它也很容易与 Windows Service 或容器的语境相连接。 如果想把应用发展成常驻应用,Generic Host 是相当自然的基础。

在做成 Windows Service 的场景中,与依赖当前目录去查找文件相比, 以 IHostEnvironment.ContentRootPath 为起点来考虑会更不容易出问题。 因为“应用的基准路径”是在 host 的语境下确定的。

常驻 worker 的基础说明常驻 worker 与定期执行用 Generic Host 加 BackgroundService 的组合最自然,也容易与 Windows Service 或容器的语境相连接。常驻 worker / 定期执行Generic Host 与 BackgroundServiceWindows Service容器常驻以 ContentRootPath 为起点查找

图14:常驻处理用 host 加 BackgroundService 的组合最自然,也容易向 Windows Service 或容器发展。

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 Service / 容器常驻的应用
  • 想与 ASP.NET Core 保持同一套扩展方法风格的应用

共同点在于, “不想草率对待应用的入口和生命周期管理”。

话虽如此,只有这些还不太好划线,所以再给出一组判断标准。如果下面这些当中有两条以上符合,那么一开始就把应用放到 Generic Host 上,之后往往会轻松很多。

采用与否的判断标准说明如果判断标准表中有两条以上符合就一开始把应用放到 Generic Host 上,如果一条都不符合则可以认为不需要它的判断流程。两条以上一条都没有数一数符合判断标准的条数一开始就放到 Generic Host 上可以认为不需要 Generic Host

图16:判断标准符合两条以上就一开始放上去,一条都不符合就不必引入。

判断标准 具体的界线
配置的数量 随环境变化的配置有 3 个以上(连接目标、阈值、输出目标等)
日志 需要留存到文件或 Event Log。不是写到标准输出就完事
运行形态 常驻运行。或者每天至少一次,按固定间隔运行
依赖关系 想通过构造函数接收的对象有 3 个以上。有想在测试中替换掉的对象
生命周期 在 Ctrl+C 或服务停止时,需要做中途的收尾工作
将来 有可能在 Windows Service 或容器中运行

8. 不适合 / 过度使用的场景

反过来,也有一些场景不需要一开始就以 Generic Host 为主角。

  • 只读取一次参数、输出一次结果就结束的小工具
  • 只使用数十分钟的粗略验证代码
  • 类库项目
  • 只读取一个配置项,用不到 DI、日志、生命周期管理的场景

在这些场景中,比起搭建 host,直接写在 Main 里,需要读的代码量和文件数都更少。作为判断标准,如果第 7 章的表中一条都不符合,那么完全可以认为不需要 Generic Host。

重要的是, Generic Host 虽然强大,但并非所有可执行文件都必须使用它。

9. 容易踩坑的地方

最后,整理一下初次使用 Generic Host 时容易踩的坑。

  • 只把 Generic Host 看作 DI 容器
    • 实际上,它是包含启动、停止、配置、日志、hosted service 的基础设施。
  • 明明是新应用,却出于惯性从 Host.CreateDefaultBuilder 开始
    • 如果没有需要贴合既有代码的理由,先从 Host.CreateApplicationBuilder 开始会更自然。
  • 在 BackgroundService 中直接放入 scoped 服务
    • hosted service 没有默认的 scope。用 IServiceScopeFactory 创建 scope 会更安全。
  • 只执行一次就结束的 worker,却没有把停止信号告知 host
    • 如果用 Worker 模板实现“run once”,在工作完成时不调用 IHostApplicationLifetime.StopApplication(),host 就会一直运行下去。
  • 想正常退出,却用 Environment.Exit 直接切断
    • 既然使用了 host,想干净退出时,用 StopApplication() 会更合理。
  • 在 Windows Service 中以当前目录为前提
    • 以 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,再在其中解析任务一侧的服务。如果想使用像 DbContext 这样的 scoped 服务,尤其需要注意这种写法。

作者简介

本文作者的个人简介页面。

Go Komura

小村软件有限公司 代表

以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表