.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 及以后版本的实务感受为前提,先整理清楚下面这四点。
- Generic Host 的真正面貌
- 它统一负责照看的是什么
Host.CreateApplicationBuilder/Host.CreateDefaultBuilder/WebApplication.CreateBuilder之间的关系- 从哪里入手比较稳妥
目录
- 先说结论(一句话)
- 先看整理表
- 2.1. Generic Host 承担的内容
- 2.2. builder 之间的区别
- Generic Host 的整体概览(图)
- Generic Host 好在哪里
- 4.1. 可以把启动处理集中到一处
- 4.2. DI / 配置 / 日志从一开始就连在一起
- 4.3. 便于处理正常退出与常驻运行
- 最小配置
- 5.1. 在控制台应用中使用的最小示例
- 5.2.
appsettings.json - 5.3. 加载
BackgroundService
- 典型模式
- 6.1. 短命的控制台工具
- 6.2. worker / 后台服务
- 6.3. 也存在于 ASP.NET Core 之下
- 适合的场景
- 不适合 / 过度使用的场景
- 容易踩坑的地方
- 总结
- 参考资料
1. 先说结论(一句话)
- Generic Host 是统一处理 .NET 应用启动与生命周期的基础设施。
- 其中包含 DI、配置、日志、
IHostedService/BackgroundService,以及应用的停止处理。 - 对于新的非 Web 应用,直接从
Host.CreateApplicationBuilder(args)入手会更自然。 - ASP.NET Core 的
WebApplicationBuilder也并非另一个世界的东西,而是把同样的 host 理念扩展到 Web 场景的一个入口。 - 也就是说,Generic Host 并不只是 DI 容器本身的话题,而是统一管理应用组装点与生命周期的机制。
归根结底,一旦应用超出了「读取参数、输出一次就结束」的范畴,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 封装」。 实际上,把它看作是统一配置应用入口周边事务的一个箱子,会最不容易出错。
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 场景的入口。
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) 这一步,其实已经加载了相当多的内容。
默认情况下,例如会包含以下这些:
- 内容根目录是当前目录
- host 配置来自带
DOTNET_前缀的环境变量和命令行参数 - 应用配置来自
appsettings.json、appsettings.{Environment}.json、Development 环境下的 user secrets、环境变量、命令行参数 - 日志输出到 Console / Debug / EventSource / EventLog(仅 Windows)
- 在
Development环境下会进行 scope 验证和依赖关系验证
也就是说,并不是从零开始手动搭建所有配线, 而是一开始就放置好了一个「日常使用基本足够」的基础。
4. Generic Host 好在哪里
4.1. 可以把启动处理集中到一处
Generic Host 最不起眼却影响最大的好处,就是应用的入口不容易变得分散。
随着应用的发展,Main 周围通常会逐渐增加以下内容。
- 读取配置文件
- 按环境替换配置
- 初始化 logger
- 组装
HttpClient、repository、service - 启动后台处理
- 收到结束信号时的收尾工作
如果不用 host,把这些全部手动连接起来, 一开始可能还比较轻松,但入口会慢慢变得越来越难缠。
使用 Generic Host 之后,Program.cs 就能清晰地成为「统一组装依赖关系的地方」。
仅凭这一点整理,代码评审的难易度就会有很大改善。
4.2. DI / 配置 / 日志从一开始就连在一起
使用 Generic Host 之后,DI、配置、日志从一开始就建立在同一个基础之上。
例如在类的一侧,可以很自然地接收下面这些:
ILogger<T>IConfigurationIHostEnvironmentIOptions<T>
这里的好处在于,读取配置的方式和创建服务的方式不容易变成两套不同的风格。
如果只有一两个配置项,直接读取 IConfiguration["Section:Key"] 也能正常运行。
但在实际项目中配置项一旦增多,按 section 把配置绑定到类上、再通过 IOptions<T> 使用,会更稳妥。
同样,日志也不必到处手动创建 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。
即使是只运行一次的控制台工具, 如果想要 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;
}
}
如果不需要长期常驻,也不必走到 RunAsync() 这一步。
Build() 之后解析所需的服务,完成工作就直接结束。
这样同样能充分享受到 Generic Host 的好处。
这里其实相当重要。 不需要给每一个短命的任务都套上 Worker 模板。
5.2. appsettings.json
对于上面的示例,配置文件用下面这种最小形式就足够了。
{
"Sample": {
"Message": "hello from Generic Host"
}
}
这个示例中直接读取的是 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;
}
}
这个示例中要关注两点。
BackgroundService的主体是ExecuteAsync- 如果需要 scoped 依赖关系,用
IServiceScopeFactory来创建 scope
BackgroundService 本身没有默认的 scope。
比如想使用像 DbContext 这样的 scoped 服务时,
像上面这样在 scope 内解析任务一侧的服务会更安全。
另外,定期执行本身工具的选择是另一个话题,
如果以 async 为基础编写,PeriodicTimer 会相当稳妥。
这一点也与相关文章中的定时器话题相连。
6. 典型模式
6.1. 短命的控制台工具
像批处理、转换工具、维护命令这类只执行一次工作就结束的应用, 同样可以正常使用 Generic Host。
适合的场景包括:
- 想读取配置文件
- 想输出日志
- 想注入
HttpClient或 repository - 想返回结束代码
对于这类应用,如果一开始就引入 BackgroundService 和 RunAsync(),
会显得有些沉重,也会过度使用 host 的生命周期管理。
对于短命的任务,像前面的最小示例那样解析并执行 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 虽然强大,但并非所有可执行文件都必须使用它。
9. 容易踩坑的地方
最后,整理一下初次使用 Generic Host 时容易踩的坑。
- 只把 Generic Host 看作 DI 容器
- 实际上,它是包含启动、停止、配置、日志、hosted service 的基础设施。
- 明明是新应用,却出于惯性从
Host.CreateDefaultBuilder开始- 如果没有需要贴合既有代码的理由,先从
Host.CreateApplicationBuilder开始会更自然。
- 如果没有需要贴合既有代码的理由,先从
- 在
BackgroundService中直接放入 scoped 服务- 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 中以当前目录为前提
- 以
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 的判断表
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
在桌面应用中使用 .NET Generic Host 与 BackgroundService 的理由
本文整理在 Windows 工具或常驻应用中,如何使用 Generic Host 与 BackgroundService 来梳理启动、定期处理、退出处理、日志、配置与 DI。
如何理解 Windows 的会话隔离 ── Session 0・RDP・多用户同时运行
整理 Windows 应用程序开发者容易混淆的「会话」概念。说明服务无法显示 UI 的 Session 0 隔离原因、RDP 连接时会话的行为、命名对象的会话隔离,以及共享 PC・RDS 环境中常见的设计失误,从实务角度解说。
Windows 的进程间通信该怎么选 ── 命名管道 / TCP / gRPC / 共享内存 / COM 判断表
整理 Windows 应用程序之间应该如何选择通信方式。用判断表整理命名管道、本地 TCP、gRPC、共享内存、文件对接、COM 各自的优势与陷阱,并从实务角度说明 UI+服务分离・32bit/64bit 桥接・权限边界等常见架构,以及命名管道的实现示例。
Windows 服务的创建与运维 ── 从任务计划程序的取舍到 BackgroundService 服务化
常驻处理应该做成 Windows 服务,还是任务计划程序就够用?本文整理判断表,以及用 .NET Worker Service(UseWindowsService)创建服务的方法、运行账户、恢复选项、事件日志、安全停止处理,从实务角度梳理投入运维所需的设计。
Windows应用的数据存储位置怎么选 ── SQLite / JSON / 注册表 / Access 判断表
Windows桌面应用的数据该存在哪里、用什么格式保存?本文整理AppData/ProgramData的使用区分,以及SQLite、JSON文件、注册表、Access(.accdb)各自的优势与陷阱,并附判断表,从实务角度讲解防止数据损坏与位数问题等注意事项。
常见问题
汇总了咨询这一主题时常见的问题。
- 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 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。