4、依赖注入与配置:内置DI容器、服务生命周期、IOptions模式与配置文件读取
依赖注入(DI)和配置管理,是 .NET 云原生应用的基石。说白了,它们解决的是「对象怎么创建」和「配置怎么读取」这两个最基础的问题。我刚开始接触 .NET Core 时,也被各种生命周期搞得晕头转向。今天咱们就把它彻底理清楚。
4.1 内置DI容器:为什么我们需要它?
你想想看,一个典型的云原生应用,可能有几十个服务类。如果每个类都自己 new 依赖的对象,代码会变成什么样?
public class OrderService
{
private readonly ILogger _logger = new ConsoleLogger();
private readonly IEmailSender _emailSender = new SmtpEmailSender();
public void ProcessOrder(Order order)
{
_logger.Log("开始处理订单");
_emailSender.Send("admin@example.com", "新订单");
}
}
这样写有什么问题?耦合太紧。哪天你想把 ConsoleLogger 换成 FileLogger,得改所有用到的地方。我在项目中遇到过,一个老项目就是因为这种写法,换个日志库改了三天。
DI 容器的核心思想很简单:你只管声明需要什么,容器负责给你什么。.NET 内置的 DI 容器在 Microsoft.Extensions.DependencyInjection 包里,开箱即用。
核心注册方式:
AddSingleton<TService, TImplementation>()- 全局唯一实例AddScoped<TService, TImplementation>()- 每个请求/作用域一个实例AddTransient<TService, TImplementation>()- 每次注入都创建新实例
4.2 服务生命周期:选错了会出大事
生命周期是 DI 里最容易踩坑的地方。我给大家讲个真实案例。
曾经有个同事,把一个数据库上下文(DbContext)注册成了 Singleton。结果呢?多个请求共享同一个上下文,数据全乱套了。因为 DbContext 不是线程安全的,它应该用 Scoped 生命周期。
三种生命周期的区别,我用一张表说清楚:
| 生命周期 | 创建时机 | 典型用途 | 注意事项 |
|---|---|---|---|
| Transient | 每次注入时 | 轻量级、无状态服务 | 频繁创建有性能开销 |
| Scoped | 每个请求开始时 | DbContext、工作单元 | 不要在 Singleton 中注入 Scoped |
| Singleton | 应用启动时 | 配置对象、日志、缓存 | 注意线程安全 |
避坑指南:我曾经犯过一个错误——在 Singleton 服务里注入了 Scoped 的 DbContext。结果第一个请求正常,第二个请求就报「DbContext 已被释放」。因为 Singleton 长期存活,而 Scoped 在请求结束后就被释放了。解决方案是用 IServiceScopeFactory 手动创建作用域。
为什么会这样?因为 Singleton 的实例是应用级别的,它引用的 Scoped 对象在请求结束后就被容器回收了。嗯,这里要注意:永远不要让长生命周期的服务引用短生命周期的服务。
4.3 IOptions模式:配置管理的正确姿势
配置文件读取,看起来简单,但要做好并不容易。我个人习惯用 IOptions 模式,它比直接读 IConfiguration 优雅得多。
先看一个典型的 appsettings.json:
{
"SmtpSettings": {
"Host": "smtp.example.com",
"Port": 587,
"UserName": "admin",
"Password": "******",
"UseSsl": true
}
}
然后定义一个强类型配置类:
public class SmtpSettings
{
public string Host { get; set; }
public int Port { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
public bool UseSsl { get; set; }
}
在 Program.cs 里注册:
builder.Services.Configure<SmtpSettings>(
builder.Configuration.GetSection("SmtpSettings"));
使用时注入 IOptions<SmtpSettings>:
public class EmailService
{
private readonly SmtpSettings _settings;
public EmailService(IOptions<SmtpSettings> options)
{
_settings = options.Value;
}
}
个人经验:我建议用 IOptionsSnapshot<T> 代替 IOptions<T>。为什么?因为 IOptions<T> 是 Singleton 的,配置改了也不会刷新。而 IOptionsSnapshot<T> 是 Scoped 的,每次请求都会重新读取配置。在云原生环境下,配置热更新太重要了。
4.4 配置文件读取:不止是 JSON
.NET 的配置系统非常灵活。它支持多种配置源,而且可以叠加:
- JSON 文件:最常用,支持多环境(appsettings.Development.json)
- 环境变量:适合容器化部署,比如 Docker 里传配置
- 命令行参数:临时覆盖配置
- Azure Key Vault:敏感信息管理
配置的优先级是:后添加的覆盖先添加的。默认顺序是:appsettings.json → appsettings.{Environment}.json → 环境变量 → 命令行参数。
举个例子,你在开发环境用 JSON 文件,生产环境用环境变量覆盖数据库连接串:
// 环境变量格式
ConnectionStrings__DefaultConnection=Server=prod-db;Database=MyDb;...
// 在 Docker Compose 里
environment:
- ConnectionStrings__DefaultConnection=Server=prod-db;...
注意那个双下划线 __,这是环境变量配置的分层分隔符。嗯,我第一次看到也觉得奇怪,但用习惯了就觉得很顺手。
4.5 实战:构建一个可配置的云原生服务
最后,咱们把前面学的串起来。假设我们要写一个「订单通知服务」,它需要发邮件和短信:
// 1. 定义配置类
public class NotificationSettings
{
public bool EnableEmail { get; set; }
public bool EnableSms { get; set; }
public SmtpSettings Smtp { get; set; }
public SmsSettings Sms { get; set; }
}
// 2. 注册服务
builder.Services.Configure<NotificationSettings>(
builder.Configuration.GetSection("Notification"));
builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();
builder.Services.AddScoped<ISmsSender, TwilioSmsSender>();
builder.Services.AddScoped<INotificationService, NotificationService>();
// 3. 使用
public class NotificationService : INotificationService
{
private readonly NotificationSettings _settings;
private readonly IEmailSender _emailSender;
private readonly ISmsSender _smsSender;
public NotificationService(
IOptionsSnapshot<NotificationSettings> options,
IEmailSender emailSender,
ISmsSender smsSender)
{
_settings = options.Value;
_emailSender = emailSender;
_smsSender = smsSender;
}
public async Task NotifyAsync(Order order)
{
if (_settings.EnableEmail)
await _emailSender.SendAsync(order);
if (_settings.EnableSms)
await _smsSender.SendAsync(order);
}
}
核心要点回顾:
- DI 容器帮你管理对象的创建和生命周期
- Singleton、Scoped、Transient 各有适用场景,别搞混
- IOptions 模式让配置强类型化,推荐用 IOptionsSnapshot 支持热更新
- 配置源可以叠加,环境变量在容器化部署中特别好用
说实话,依赖注入和配置管理,是 .NET 云原生开发的「基本功」。你想想看,一个微服务系统里,几十个服务互相调用,如果没有 DI 容器,手动管理依赖关系会是什么噩梦?
我个人习惯是:所有服务都通过构造函数注入,绝不手动 new 依赖对象。这样代码可测试性极强,单元测试时只需要 mock 接口就行。
下一章,咱们聊聊中间件管道和请求处理。嗯,那又是另一个有意思的话题了。