第 12 章:依赖注入的原理与实现
jerry北京市2026年4月21日C# 9 次阅读 约 24 分钟

深入理解 IoC 和 DI 的概念、服务生命周期(Transient/Scoped/Singleton)、Microsoft.Extensions.DependencyInjection 的内部实现、常见陷阱,以及 .NET 8 的 Keyed Services。
12.1 IoC 与 DI 概念
控制反转(Inversion of Control)
传统方式:类自己创建依赖(控制权在类内部):
// 紧耦合:OrderService 直接依赖具体实现
class OrderService
{
private readonly SqlOrderRepository _repo = new SqlOrderRepository(); // 硬编码依赖
private readonly EmailNotifier _notifier = new EmailNotifier();
public void PlaceOrder(Order order)
{
_repo.Save(order);
_notifier.Notify(order);
}
}
IoC 方式:依赖从外部注入(控制权反转到外部):
// 松耦合:依赖接口,由外部注入
class OrderService
{
private readonly IOrderRepository _repo;
private readonly INotifier _notifier;
public OrderService(IOrderRepository repo, INotifier notifier) // 构造函数注入
{
_repo = repo;
_notifier = notifier;
}
public void PlaceOrder(Order order)
{
_repo.Save(order);
_notifier.Notify(order);
}
}
注入方式
// 1. 构造函数注入(推荐)
class MyService(ILogger<MyService> logger, IRepository repo)
{
// 使用 C# 12 主构造函数
}
// 2. 方法注入(特定场景)
class MyService
{
public void Process([FromServices] IValidator validator) { }
}
// 3. 属性注入(不推荐,.NET DI 不原生支持)
class MyService
{
[Inject]
public ILogger Logger { get; set; } // 需要第三方容器如 Autofac
}
12.2 服务生命周期
三种生命周期
var builder = WebApplication.CreateBuilder(args);
// Transient:每次请求都创建新实例
builder.Services.AddTransient<IEmailSender, SmtpEmailSender>();
// Scoped:每个作用域(HTTP 请求)一个实例
builder.Services.AddScoped<IDbContext, AppDbContext>();
// Singleton:整个应用生命周期一个实例
builder.Services.AddSingleton<ICacheService, MemoryCacheService>();
请求 1 请求 2
┌─────────────────────┐ ┌─────────────────────┐
│ Scoped 实例 A │ │ Scoped 实例 B │
│ │ │ │
│ Transient 实例 1 │ │ Transient 实例 3 │
│ Transient 实例 2 │ │ Transient 实例 4 │
└─────────────────────┘ └─────────────────────┘
Singleton 实例(全局唯一)
┌─────────────────────────────────────────────────┐
│ 整个应用共享同一个实例 │
└─────────────────────────────────────────────────┘
验证生命周期
class LifetimeDemo(ILogger<LifetimeDemo> logger)
{
private readonly Guid _id = Guid.NewGuid();
public void ShowId() => logger.LogInformation("实例 ID: {Id}", _id);
}
// 注册为不同生命周期,观察 ID 变化
builder.Services.AddTransient<LifetimeDemo>(); // 每次不同
builder.Services.AddScoped<LifetimeDemo>(); // 同一请求内相同
builder.Services.AddSingleton<LifetimeDemo>(); // 永远相同
12.3 Microsoft.Extensions.DI 内部实现
ServiceDescriptor
每个注册的服务都用 ServiceDescriptor 描述:
// 这三种注册方式等价
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.Add(new ServiceDescriptor(
serviceType: typeof(IUserService),
implementationType: typeof(UserService),
lifetime: ServiceLifetime.Scoped));
builder.Services.Add(ServiceDescriptor.Scoped<IUserService, UserService>());
ServiceProvider 的工作流程
解析服务的过程:
1. 查找 ServiceDescriptor(按服务类型)
2. 检查生命周期:
- Singleton → 从根容器的缓存中获取或创建
- Scoped → 从当前 Scope 的缓存中获取或创建
- Transient → 每次创建新实例
3. 递归解析构造函数参数(依赖)
4. 调用构造函数创建实例
// ServiceProvider 内部简化模型
class SimpleServiceProvider
{
private readonly Dictionary<Type, ServiceDescriptor> _descriptors;
private readonly Dictionary<Type, object> _singletons = new();
private readonly Dictionary<Type, object> _scopedInstances = new();
public object GetService(Type serviceType)
{
var descriptor = _descriptors[serviceType];
return descriptor.Lifetime switch
{
ServiceLifetime.Singleton =>
_singletons.GetOrAdd(serviceType, _ => CreateInstance(descriptor)),
ServiceLifetime.Scoped =>
_scopedInstances.GetOrAdd(serviceType, _ => CreateInstance(descriptor)),
ServiceLifetime.Transient =>
CreateInstance(descriptor),
_ => throw new InvalidOperationException()
};
}
private object CreateInstance(ServiceDescriptor descriptor)
{
// 找到构造函数,递归解析参数,创建实例
var ctor = descriptor.ImplementationType!.GetConstructors().First();
var parameters = ctor.GetParameters()
.Select(p => GetService(p.ParameterType))
.ToArray();
return ctor.Invoke(parameters);
}
}
注册的高级用法
// 工厂注册
builder.Services.AddScoped<IDbContext>(sp =>
{
var config = sp.GetRequiredService<IConfiguration>();
return new AppDbContext(config.GetConnectionString("Default")!);
});
// 注册多个实现
builder.Services.AddTransient<INotifier, EmailNotifier>();
builder.Services.AddTransient<INotifier, SmsNotifier>();
builder.Services.AddTransient<INotifier, PushNotifier>();
// 获取所有实现
class NotificationService(IEnumerable<INotifier> notifiers)
{
public async Task NotifyAll(string message)
{
foreach (var notifier in notifiers)
await notifier.SendAsync(message);
}
}
// TryAdd:如果已注册则跳过
builder.Services.TryAddScoped<IUserService, UserService>();
// Replace:替换已有注册
builder.Services.Replace(ServiceDescriptor.Scoped<IUserService, MockUserService>());
12.4 常见陷阱
陷阱 1:Captive Dependency(俘获依赖)
Singleton 服务依赖 Scoped 服务,导致 Scoped 服务的生命周期被"提升"为 Singleton:
// ❌ 错误:Singleton 持有 Scoped 的引用
builder.Services.AddSingleton<ICacheService, CacheService>();
builder.Services.AddScoped<IDbContext, AppDbContext>();
class CacheService : ICacheService
{
private readonly IDbContext _db; // Scoped 服务被 Singleton 俘获!
public CacheService(IDbContext db)
{
_db = db; // 这个 DbContext 永远不会被释放,整个应用共享同一个
}
}
// ✅ 正确:使用 IServiceScopeFactory 手动创建 Scope
class CacheService(IServiceScopeFactory scopeFactory) : ICacheService
{
public async Task<User?> GetUserAsync(int id)
{
using var scope = scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<IDbContext>();
return await db.Users.FindAsync(id);
}
}
陷阱 2:在 Scope 外解析 Scoped 服务
// ❌ 错误:从根容器解析 Scoped 服务
var app = builder.Build();
var db = app.Services.GetRequiredService<IDbContext>(); // 异常或内存泄漏
// ✅ 正确:创建 Scope
using var scope = app.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<IDbContext>();
陷阱 3:Dispose 问题
// DI 容器会自动 Dispose 它创建的 IDisposable 对象
// 但如果你用工厂方法注册,需要注意
// ❌ 可能的问题:外部创建的对象不会被容器 Dispose
var connection = new SqlConnection(connectionString);
builder.Services.AddSingleton(connection); // 容器不会 Dispose 这个对象
// ✅ 使用工厂方法,让容器管理生命周期
builder.Services.AddSingleton<SqlConnection>(_ =>
new SqlConnection(connectionString));
开启验证
var builder = WebApplication.CreateBuilder(args);
// 开发环境开启 DI 验证
if (builder.Environment.IsDevelopment())
{
builder.Host.UseDefaultServiceProvider(options =>
{
options.ValidateScopes = true; // 验证 Scope 问题
options.ValidateOnBuild = true; // 构建时验证所有服务可解析
});
}
12.5 Keyed Services(.NET 8)
.NET 8 新增按名称/键注册和解析服务:
// 注册
builder.Services.AddKeyedSingleton<ICache, RedisCache>("redis");
builder.Services.AddKeyedSingleton<ICache, MemoryCache>("memory");
// 注入(使用 [FromKeyedServices] 特性)
class ProductService(
[FromKeyedServices("redis")] ICache distributedCache,
[FromKeyedServices("memory")] ICache localCache)
{
public async Task<Product?> GetAsync(int id)
{
// 先查本地缓存
var product = localCache.Get<Product>($"product:{id}");
if (product is not null) return product;
// 再查分布式缓存
product = await distributedCache.GetAsync<Product>($"product:{id}");
if (product is not null)
{
localCache.Set($"product:{id}", product, TimeSpan.FromMinutes(1));
}
return product;
}
}
// 手动解析
var cache = app.Services.GetRequiredKeyedService<ICache>("redis");
Keyed Services 之前的变通方案
// .NET 8 之前需要用工厂模式
builder.Services.AddSingleton<RedisCache>();
builder.Services.AddSingleton<MemoryCache>();
builder.Services.AddSingleton<Func<string, ICache>>(sp => key => key switch
{
"redis" => sp.GetRequiredService<RedisCache>(),
"memory" => sp.GetRequiredService<MemoryCache>(),
_ => throw new ArgumentException($"未知的缓存类型: {key}")
});
12.6 面试要点
-
三种生命周期的区别?
- Transient:每次解析创建新实例
- Scoped:每个作用域(HTTP 请求)一个实例
- Singleton:整个应用一个实例
-
什么是 Captive Dependency?
- Singleton 依赖 Scoped/Transient 服务,导致短生命周期服务被长生命周期服务俘获
- 解决方案:使用 IServiceScopeFactory 手动创建 Scope
-
DI 容器如何解析服务?
- 查找 ServiceDescriptor → 检查缓存 → 递归解析构造函数参数 → 创建实例
-
如何注册同一接口的多个实现?
- 多次 Add 注册,通过 IEnumerable<T> 获取所有实现
- .NET 8 使用 Keyed Services 按名称区分
-
ValidateScopes 和 ValidateOnBuild 的作用?
- ValidateScopes:检测 Scope 违规(如从根容器解析 Scoped 服务)
- ValidateOnBuild:构建时验证所有注册的服务都可以被解析
练习
- 实现一个简化版的 DI 容器,支持 Transient 和 Singleton 生命周期
- 复现 Captive Dependency 问题,并用 ValidateScopes 检测
- 使用 Keyed Services 实现策略模式(根据配置选择不同的实现)
- 使用 IServiceScopeFactory 在后台任务中安全地使用 Scoped 服务
评论
登录 后发表评论
暂无评论