C#如何配置依赖注入_C#在控制台程序中使用DI容器【架构】
控制台程序使用DI容器需引用Microsoft.Extensions.Hosting包。AddScoped需配合手动CreateScope()使用,否则等同于Transient。构造函数注入仅对容器创建的对象生效,定时任务等非托管上下文必须手动创建并释放作用域,注意注册顺序与接口类型匹配。
控制台程序里确实用不上 ASP.NET Core 自带的 HTTP 请求作用域,但 IServiceCollection 搭配 ServiceProvider 这套组合拳完全能打。说到底,问题不在于“能不能用”,而在于“怎么配才不踩坑”。下面把这些常见的坑和正确的做法捋一遍。
注册服务前,先确认项目已引用 Hosting 包
控制台项目默认不带 DI 容器运行时支持,光装个 Microsoft.Extensions.DependencyInjection 是远远不够的。必须加上 Microsoft.Extensions.Hosting(.NET 6 及以上版本推荐),或者至少搭配 Microsoft.Extensions.DependencyInjection.Abstractions 并手动调用 BuildServiceProvider()。
- 项目文件里需要包含这样一行:
(版本号要和 SDK 对齐) - 没装这个包的话,
Host.CreateApplicationBuilder()一跑就会报找不到类型;硬着头皮用new ServiceCollection()虽然也能注册,但缺了 Host 环境下自动配置的日志、配置等扩展能力,等于自己给自己找麻烦 - 特别提醒:别在 .NET Framework 项目里硬套这套——
Microsoft.Extensions.*是 .NET Core 及之后生态的东西,Framework 下需要额外做适配
AddTransient/Scoped/Singleton 在控制台里怎么选
控制台程序没有 Web 项目那种“每个请求一个作用域”的自动机制,所以 AddScoped 的行为会变得有点特别——它只在你手动 CreateScope() 之后才生效,否则,它就等同于 AddTransient。
AddTransient:每次调用() GetRequiredService都新建一个实例;适合无状态工具类,比如() IMapper、IEmailSender这类AddScoped:必须搭配() using var scope = provider.CreateScope();来使用,否则注入IRepository时会直接报错,提示“没有注册IRepository类型的服务”AddSingleton:这个最安全,() IConfiguration、ILoggerFactory这类全局只读对象就该用单例。但有一个典型陷阱:千万别让 Singleton 类持有 Scoped 服务,比如把DbContext塞进单例类里,启动时就会报“无法从根作用域解析 Scoped 服务”
构造函数注入只对容器创建的对象生效
这是一个很容易被忽略的规则:你在 Main 里直接 new MessageService(new ConsoleMessageWriter()),哪怕 MessageService 的构造函数声明了 IMessageWriter,DI 也完全不参与——那个字段永远是 null,不会自动填充。
- 正确做法:先注册
MessageService(比如builder.Services.AddTransient),然后通过() provider.GetRequiredService来获取() - 中间件、后台服务、定时任务回调里想用服务?不能靠字段注入。正确做法是把
IServiceProvider注入到宿主类中(比如Worker : BackgroundService),然后调用_provider.CreateScope()来获取新作用域 - 接口和实现类型必须严格匹配:注册了
AddScoped,构造函数里就得写() IEmailService email,写成SmtpEmailService email就会报“没有注册SmtpEmailService类型的服务”
非托管上下文必须手动管理作用域生命周期
Timer 回调、BackgroundService.ExecuteAsync、单元测试这些地方,没有现成的 IScopedService 上下文,直接 Resolve Scoped 服务肯定会炸。
- 错误写法:
var repo = provider.GetRequiredService—— 会报(); No service for type 'IRepository' - 正确写法:
using var scope = provider.CreateScope(); var repo = scope.ServiceProvider.GetRequiredService(); - 必须用
using包裹作用域,否则DbContext不会释放,可能导致连接池耗尽、内存泄漏。另外,千万别把scope.ServiceProvider存成字段长期持有——它只在当前using块内有效 - 单元测试常用轻量方案:
new ServiceCollection().AddScoped().BuildServiceProvider()
还有一个容易被忽略的细节:注册顺序会影响替换逻辑。用 TryAdd 或 Replace 时,如果第三方库内部已经注册过某个服务(比如 EF 的 IDbContextFactory),你的 AddScoped 可能被静默跳过——要发现这个问题,要么去看源码,要么启用 DI 日志。


































