“不要调用我们,我们会调用你。”——这是好莱坞原则,也是 IoC(Inversion of Control)的精髓。很多人用了好几年 ASP.NET Core 的
AddScoped/AddTransient,却说不清 IoC 和 DI 的区别,更不理解容器内部到底做了什么。本文从控制反转的本质讲起,手写一个迷你容器,再深入 .NET Core DI 容器的生命周期、作用域、验证机制和高级模式,结合 PMS 项目的真实场景,把 IoC 讲透。
目录
- 控制反转到底"反转"了什么
- DI 的三种注入方式
- 生命周期:Singleton / Scoped / Transient
- Captive Dependency:最隐蔽的陷阱
- IServiceScopeFactory:手动创建作用域
- .NET Core DI 容器内部原理
- 手写迷你 DI 容器
- PMS 项目 IoC 演进:从静态类到 DI 容器
- 注册策略与程序集扫描
- Keyed Services:同一接口多实现
- 泛型服务与开放泛型
- ActivatorUtilities 与工厂模式
- 第三方容器:Autofac / DryIoc 集成
- DI 验证与诊断工具
- Checklist
1. 控制反转到底"反转"了什么
1.1 没有 IoC 的世界
假设我们要实现一个备件入库服务,需要保存数据、发通知、记录日志:
// ❌ 紧密耦合:SparePartService 直接 new 了所有依赖
public class SparePartService
{
private readonly SqlServerRepository _repository = new SqlServerRepository();
private readonly EmailNotifier _notifier = new EmailNotifier();
private readonly FileLogger _logger = new FileLogger();
public void StockIn(SparePart part)
{
_repository.Save(part);
_notifier.Notify("备件入库", part.PartName);
_logger.Log($"备件 {part.PartNo} 已入库");
}
}
问题显而易见:
- 换不了实现:想把数据库从 SQL Server 换成 PostgreSQL?改 Service 代码
- 测不了试:单元测试必须连真实数据库、真实邮件服务器
- 管不了生命周期:每次 new 一个 Repository,连接池被打爆
- 违反开闭原则:加一种通知方式就要改 Service
1.2 引入控制反转
核心思想:把对象的创建和依赖关系的管理,从对象自身"反转"给外部容器。
// ✅ 依赖抽象,不依赖具体实现
public class SparePartService
{
private readonly IRepository<SparePart> _repository;
private readonly INotifier _notifier;
private readonly ILogger<SparePartService> _logger;
// 依赖由外部注入,自己不创建
public SparePartService(
IRepository<SparePart> repository,
INotifier notifier,
ILogger<SparePartService> logger)
{
_repository = repository;
_notifier = notifier;
_logger = logger;
}
public void StockIn(SparePart part)
{
_repository.Save(part);
_notifier.Notify("备件入库", part.PartName);
_logger.LogInformation("备件 {PartNo} 已入库", part.PartNo);
}
}
1.3 IoC ≠ DI ≠ 容器
这三个概念经常被混用,但它们有明确的区别:
| 概念 | 含义 | 关系 |
|---|---|---|
| IoC(控制反转) | 一种设计原则——把控制权从业务代码转移给框架/容器 | 最上层的思想 |
| DI(依赖注入) | 实现 IoC 的一种模式——通过构造函数/属性/方法传入依赖 | IoC 的具体实现方式之一 |
| IoC 容器 | 负责注册、解析、释放依赖的框架 | DI 的基础设施 |
IoC 的其他实现方式还包括:模板方法模式、服务定位器模式、事件驱动等。DI 是最主流、最推荐的方式。
💬 互动一下:你在项目中见过服务定位器(Service Locator)模式吗?那种在类内部
IoC.Resolve<IFoo>()的写法,虽然也实现了控制反转,但隐藏了依赖关系,被称为"DI 的反面模式"——你觉得它和构造函数注入比,最大的问题是什么?
2. DI 的三种注入方式
2.1 构造函数注入(推荐)
public class SparePartService
{
private readonly IRepository<SparePart> _repository;
private readonly INotifier _notifier;
// 依赖在构造函数中声明,类创建时就必须提供
public SparePartService(
IRepository<SparePart> repository,
INotifier notifier)
{
_repository = repository;
_notifier = notifier;
}
}
优点:
- 依赖一目了然,看构造函数就知道这个类需要什么
- 依赖声明为
readonly,创建后不可变 - 容器在创建时就能检测缺失依赖,快速失败
- 单元测试时直接传入 Mock 即可
适用场景:必选依赖(类没有这些依赖就无法工作)。
2.2 属性注入(谨慎使用)
public class SparePartService
{
// 可选依赖,通过属性设置
public IAuditLogger? AuditLogger { get; set; }
private readonly IRepository<SparePart> _repository;
public SparePartService(IRepository<SparePart> repository)
{
_repository = repository;
}
}
.NET Core DI 原生不支持属性注入,需要第三方容器(如 Autofac)。
适用场景:
- 可选依赖(有则用,没有也不影响核心功能)
- 循环依赖的变通方案(但更好的做法是重构消除循环依赖)
风险:属性可能为 null,使用时必须判空。
2.3 方法注入(特定场景)
public class SparePartService
{
public void StockIn(
SparePart part,
[FromKeyedServices("email")] INotifier notifier)
{
notifier.Notify("备件入库", part.PartName);
}
}
ASP.NET Core 中 [FromServices] 也是一种方法注入:
[HttpPost]
public IActionResult StockIn(
[FromBody] SparePartDto dto,
[FromServices] ISparePartService service)
{
service.StockIn(dto);
return Ok();
}
适用场景:仅在某个方法中使用的依赖,不想让整个类都持有它。
2.4 选择原则
必选依赖 → 构造函数注入
可选依赖 → 属性注入(或构造函数中给默认值)
方法级依赖 → [FromServices] / [FromKeyedServices]
99% 的场景用构造函数注入就够了。 如果一个类的构造函数参数超过 5 个,往往说明这个类职责过多,应该考虑拆分。
3. 生命周期:Singleton / Scoped / Transient
3.1 三种生命周期对比
// Singleton:整个应用程序生命周期内只有一个实例
builder.Services.AddSingleton<ICacheService, RedisCacheService>();
// Scoped:每个请求(作用域)一个实例
builder.Services.AddScoped<IRepository<SparePart>, EfRepository<SparePart>>();
// Transient:每次请求都创建新实例
builder.Services.AddTransient<IEmailSender, SmtpEmailSender>();
| 生命周期 | 创建时机 | 释放时机 | 适用场景 |
|---|---|---|---|
| Singleton | 首次解析时 | 应用关闭时 | 无状态服务、缓存、配置 |
| Scoped | 每个 Scope 首次解析时 | Scope 释放时 | DbContext、仓储、工作单元 |
| Transient | 每次解析时 | Scope 释放时(如果实现了 IDisposable) | 轻量无状态服务 |
3.2 验证生命周期
用最简单的代码验证三种生命周期的行为:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<ISingletonService, SingletonService>();
builder.Services.AddScoped<IScopedService, ScopedService>();
builder.Services.AddTransient<ITransientService, TransientService>();
var app = builder.Build();
app.MapGet("/lifetime", (
ISingletonService singleton1,
ISingletonService singleton2,
IScopedService scoped1,
IScopedService scoped2,
ITransientService transient1,
ITransientService transient2) =>
{
return new
{
SingletonSame = ReferenceEquals(singleton1, singleton2), // true
ScopedSame = ReferenceEquals(scoped1, scoped2), // true
TransientSame = ReferenceEquals(transient1, transient2), // false
SingletonId = singleton1.Id,
ScopedId = scoped1.Id,
Transient1Id = transient1.Id,
Transient2Id = transient2.Id
};
});
app.Run();
连续请求两次:
- Singleton Id 两次相同
- Scoped Id 同一次请求内相同,不同请求不同
- Transient Id 每次注入都不同
3.3 DbContext 为什么默认是 Scoped
// EF Core AddDbContext 默认注册为 Scoped
builder.Services.AddDbContext<PmsDbContext>(options =>
options.UseSqlServer(connStr));
// 等价于:builder.Services.AddScoped<PmsDbContext>();
原因:
- 线程安全:DbContext 不是线程安全的,Scoped 保证同一时刻只有一个请求使用它
- 变更跟踪:Change Tracker 跟踪同一请求内的所有实体变更,SaveChanges 时统一提交
- 事务边界:一个请求通常是一个业务事务,Scoped 生命周期与事务边界对齐
- 连接管理:DbContext 内部管理数据库连接,Scoped 确保连接及时释放
3.4 Transient 的 IDisposable 陷阱
public class FileExporter : IFileExporter, IDisposable
{
private readonly FileStream _stream;
public FileExporter() { _stream = File.OpenWrite("export.tmp"); }
public void Dispose() => _stream.Dispose();
}
builder.Services.AddTransient<IFileExporter, FileExporter>();
⚠️ 关键细节:.NET Core DI 容器会跟踪所有它创建的 IDisposable 对象,即使是 Transient,也会在 Scope(请求)结束时才释放。如果你在一个 Singleton 服务中反复解析 Transient 的 IDisposable 对象,它们会被 Singleton 的 Scope(根容器)持有,直到应用关闭才释放——内存泄漏。
解决方式:
// 方式1:用工厂注册,告诉容器不要跟踪
builder.Services.AddTransient<IFileExporter>(sp =>
ActivatorUtilities.CreateInstance<FileExporter>(sp));
// 方式2:不实现 IDisposable,或者让消费者负责释放
// 方式3:用 using var scope = _scopeFactory.CreateScope() 在短作用域内解析
4. Captive Dependency:最隐蔽的陷阱
4.1 什么是 Captive Dependency
当一个长生命周期的服务依赖了一个短生命周期的服务,短生命周期服务被"俘虏"在长生命周期的作用域中,无法及时释放——这就是 Captive Dependency(俘虏依赖)。
// ❌ 经典错误:Singleton 依赖 Scoped
builder.Services.AddSingleton<ICacheWarmer, CacheWarmer>();
builder.Services.AddScoped<PmsDbContext>();
public class CacheWarmer : ICacheWarmer
{
private readonly PmsDbContext _dbContext;
// CacheWarmer 是 Singleton,只会被创建一次
// 但它注入了 Scoped 的 DbContext
// 结果:DbContext 被俘虏,整个应用生命周期用同一个实例
public CacheWarmer(PmsDbContext dbContext)
{
_dbContext = dbContext;
}
public void Warm()
{
// 第一次请求正常,之后 DbContext 的 Change Tracker 会越来越大
// 最终导致内存泄漏和并发异常
var parts = _dbContext.SpareParts.ToList();
}
}
4.2 实际案例:PMS 项目的 Singleton 中间件
// ❌ 中间件默认是 Singleton!
public class ShipContextMiddleware
{
private readonly RequestDelegate _next;
private readonly PmsDbContext _dbContext; // 陷阱!
public ShipContextMiddleware(RequestDelegate next, PmsDbContext dbContext)
{
_next = next;
_dbContext = dbContext; // Scoped 被 Singleton 俘虏
}
public async Task InvokeAsync(HttpContext context)
{
var shipId = context.Request.Headers["X-Ship-Id"];
// 多个请求共享同一个 DbContext → 并发异常!
var ship = await _dbContext.Ships.FindAsync(long.Parse(shipId));
await _next(context);
}
}
4.3 修复方式
// ✅ 方式1:在 InvokeAsync 中注入 Scoped 依赖
public class ShipContextMiddleware
{
private readonly RequestDelegate _next;
public ShipContextMiddleware(RequestDelegate next)
{
_next = next;
}
// 中间件的 Invoke/InvokeAsync 是按请求执行的
// 可以在这里注入 Scoped 服务
public async Task InvokeAsync(
HttpContext context,
PmsDbContext dbContext) // ✅ 每次请求解析
{
var shipId = context.Request.Headers["X-Ship-Id"];
var ship = await dbContext.Ships.FindAsync(long.Parse(shipId));
await _next(context);
}
}
// ✅ 方式2:用 IServiceScopeFactory
public class CacheWarmer : ICacheWarmer
{
private readonly IServiceScopeFactory _scopeFactory;
public CacheWarmer(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
public void Warm()
{
using var scope = _scopeFactory.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<PmsDbContext>();
var parts = dbContext.SpareParts.AsNoTracking().ToList();
// scope 释放时,DbContext 也被释放
}
}
4.4 生命周期规则速查
Singleton → 可以依赖 Singleton(✅)
Singleton → 依赖 Scoped(❌ Captive Dependency)
Singleton → 依赖 Transient(⚠️ 看情况,IDisposable 会泄漏)
Scoped → 可以依赖 Singleton(✅)
Scoped → 可以依赖 Scoped(✅)
Scoped → 可以依赖 Transient(✅)
Transient → 可以依赖任何(✅)
一句话规则:生命周期短的不能被生命周期长的持有。
4.5 开启验证
开发环境开启范围验证,启动时就能发现 Captive Dependency:
builder.WebHost.UseDefaultServiceProvider((context, options) =>
{
options.ValidateScopes = context.HostingEnvironment.IsDevelopment();
options.ValidateOnBuild = true; // 启动时验证所有依赖能否解析
});
开启后,如果存在 Singleton 依赖 Scoped 的情况,启动时直接抛异常:
InvalidOperationException: Cannot consume scoped service 'PmsDbContext'
from singleton 'ICacheWarmer'.
💬 互动一下:你有没有被 Captive Dependency 坑过?我见过一个真实案例:一个 Singleton 的 EventAggregator 持有了 Scoped 仓储的委托,运行 3 天后内存从 200MB 涨到 4GB,排查了整整一周。评论区聊聊你的经历。
5. IServiceScopeFactory:手动创建作用域
5.1 什么时候需要手动创建 Scope
- 后台任务(
BackgroundService或IHostedService):没有 HTTP 请求上下文,需要自己创建 Scope - Singleton 服务中需要访问 Scoped 服务
- 需要在子作用域中隔离一批操作(如批量处理,每批一个 Scope)
5.2 后台任务中的标准模式
public class ShipSyncBackgroundService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<ShipSyncBackgroundService> _logger;
public ShipSyncBackgroundService(
IServiceScopeFactory scopeFactory,
ILogger<ShipSyncBackgroundService> logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
// 每次同步创建独立 Scope
using var scope = _scopeFactory.CreateScope();
// 从 Scope 中解析 Scoped 服务
var syncService = scope.ServiceProvider
.GetRequiredService<ISyncService>();
var dbContext = scope.ServiceProvider
.GetRequiredService<PmsDbContext>();
await syncService.SyncAllShipsAsync(stoppingToken);
// scope.Dispose() 会释放所有 Scoped 和 Transient 的 IDisposable
}
catch (Exception ex)
{
_logger.LogError(ex, "船岸同步失败");
}
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
}
}
5.3 批量处理的作用域管理
public async Task BatchProcessAsync(
IEnumerable<long> partIds, CancellationToken ct)
{
var batches = partIds.Chunk(100); // 每100条一批
foreach (var batch in batches)
{
// 每批创建独立 Scope,避免 Change Tracker 膨胀
using var scope = _scopeFactory.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<PmsDbContext>();
var service = scope.ServiceProvider.GetRequiredService<ISparePartService>();
await service.ProcessBatchAsync(batch, ct);
await dbContext.SaveChangesAsync(ct);
// scope 释放,DbContext 和跟踪的实体全部释放
}
}
5.4 AsyncLocal 与 Scope 的关系
.NET Core DI 的 Scope 是基于 AsyncLocal<ServiceProviderEngineScope> 实现的。这意味着:
// 在 async/await 中,Scope 会自动流动到下游
using var scope = _scopeFactory.CreateScope();
var service = scope.ServiceProvider.GetRequiredService<ISparePartService>();
// 即使在异步方法中,AsyncLocal 也会传播当前 Scope
await service.DoWorkAsync(); // 内部解析的 Scoped 服务来自同一个 Scope
但要注意 Task.Run 不一定会传播 AsyncLocal(取决于 ExecutionContext 的流动),最安全的方式是在 Task.Run 内部创建新 Scope。
6. .NET Core DI 容器内部原理
6.1 容器的两个核心接口
// 注册服务
public interface IServiceCollection : IList<ServiceDescriptor>
{
// 本质就是一个 ServiceDescriptor 列表
}
public class ServiceDescriptor
{
public Type ServiceType { get; set; } // 接口类型(如 IRepository<SparePart>)
public Type? ImplementationType { get; set; } // 实现类型(如 EfRepository<SparePart>)
public object? ImplementationInstance { get; set; } // 单例实例
public Func<IServiceProvider, object>? ImplementationFactory { get; set; } // 工厂
public ServiceLifetime Lifetime { get; set; } // 生命周期
}
// 解析服务
public interface IServiceProvider
{
object? GetService(Type serviceType); // 找不到返回 null
}
6.2 注册到解析的完整流程
// 1. 注册阶段:只是往列表里加描述
builder.Services.AddScoped<IRepository<SparePart>, EfRepository<SparePart>>();
// 等价于:
builder.Services.Add(new ServiceDescriptor(
serviceType: typeof(IRepository<SparePart>),
implementationType: typeof(EfRepository<SparePart>),
lifetime: ServiceLifetime.Scoped));
// 2. Build 阶段:编译表达式树,生成委托工厂
// 容器会分析 EfRepository<SparePart> 的构造函数
// 生成类似这样的委托:
// (IServiceProvider sp) => new EfRepository<SparePart>(
// sp.GetRequiredService<PmsDbContext>(),
// sp.GetRequiredService<ILogger<EfRepository<SparePart>>>()
// );
// 3. 解析阶段:调用委托,缓存单例
var repo = serviceProvider.GetRequiredService<IRepository<SparePart>>();
6.3 表达式树编译
.NET Core DI 容器不使用反射 Emit,而是使用表达式树编译来创建对象。首次解析时编译,后续直接调用编译后的委托,性能接近 new。
简化版的编译逻辑:
public object CreateInstance(IServiceProvider sp, Type implementationType)
{
var constructor = implementationType
.GetConstructors()
.OrderByDescending(c => c.GetParameters().Length)
.First();
var parameters = constructor.GetParameters()
.Select(p =>
{
// 递归解析每个构造函数参数
var service = sp.GetService(p.ParameterType);
if (service == null && !p.IsOptional)
throw new InvalidOperationException(
$"无法解析 {p.ParameterType} for {implementationType}");
return service!;
})
.ToArray();
return constructor.Invoke(parameters);
}
实际的容器还会处理:
- 循环依赖检测
- 泛型参数推断
- 开放泛型匹配
- 作用域级别的实例缓存
- Disposable 跟踪
7. 手写迷你 DI 容器
为了真正理解容器原理,我们手写一个支持三种生命周期的迷你容器:
public enum LifeTime { Singleton, Scoped, Transient }
public class ServiceRegistration
{
public Type ServiceType { get; set; } = null!;
public Type ImplementationType { get; set; } = null!;
public LifeTime Lifetime { get; set; }
}
public class MiniContainer : IServiceProvider, IDisposable
{
private readonly List<ServiceRegistration> _registrations = new();
private readonly Dictionary<Type, object> _singletonCache = new();
private readonly Dictionary<Type, object> _scopedCache = new();
private readonly List<IDisposable> _disposables = new();
private bool _isRoot = true;
public void Register<TService, TImpl>(LifeTime lifetime = LifeTime.Transient)
where TImpl : TService
{
_registrations.Add(new ServiceRegistration
{
ServiceType = typeof(TService),
ImplementationType = typeof(TImpl),
Lifetime = lifetime
});
}
public object? GetService(Type serviceType)
{
var reg = _registrations.FirstOrDefault(r => r.ServiceType == serviceType)
?? throw new InvalidOperationException($"未注册 {serviceType.Name}");
// Singleton:根容器缓存
if (reg.Lifetime == LifeTime.Singleton)
{
if (!_singletonCache.TryGetValue(serviceType, out var instance))
{
instance = CreateInstance(reg.ImplementationType);
_singletonCache[serviceType] = instance;
if (instance is IDisposable d) _disposables.Add(d);
}
return instance;
}
// Scoped:当前作用域缓存
if (reg.Lifetime == LifeTime.Scoped)
{
if (_isRoot)
throw new InvalidOperationException("不能从根容器解析 Scoped 服务");
if (!_scopedCache.TryGetValue(serviceType, out var instance))
{
instance = CreateInstance(reg.ImplementationType);
_scopedCache[serviceType] = instance;
if (instance is IDisposable d) _disposables.Add(d);
}
return instance;
}
// Transient:每次创建
var transient = CreateInstance(reg.ImplementationType);
if (transient is IDisposable disposable) _disposables.Add(disposable);
return transient;
}
private object CreateInstance(Type implementationType)
{
var ctor = implementationType
.GetConstructors()
.OrderByDescending(c => c.GetParameters().Length)
.First();
var args = ctor.GetParameters()
.Select(p => GetService(p.ParameterType))
.ToArray();
return ctor.Invoke(args);
}
public MiniContainer CreateScope()
{
var scope = new MiniContainer
{
_registrations = _registrations, // 共享注册
_singletonCache = _singletonCache, // 共享单例
_isRoot = false
};
return scope;
}
public void Dispose()
{
foreach (var d in _disposables) d.Dispose();
_disposables.Clear();
_scopedCache.Clear();
if (_isRoot) _singletonCache.Clear();
}
}
使用:
var container = new MiniContainer();
container.Register<ISparePartService, SparePartService>(LifeTime.Scoped);
container.Register<IRepository<SparePart>, EfRepository<SparePart>>(LifeTime.Scoped);
container.Register<ICacheService, RedisCacheService>(LifeTime.Singleton);
container.Register<IEmailSender, SmtpEmailSender>(LifeTime.Transient);
using var scope = container.CreateScope();
var service = scope.GetService(typeof(ISparePartService)) as ISparePartService;
service!.StockIn(new SparePart());
这个迷你容器缺少很多特性(开放泛型、Keyed Services、工厂注册、循环依赖检测等),但核心原理和 .NET Core 容器是一样的:注册描述 → 构造函数分析 → 递归解析 → 生命周期缓存 → Dispose 跟踪。
8. PMS 项目 IoC 演进:从静态类到 DI 容器
8.1 旧架构:静态 IoC 类
PMS 项目早期使用了一个静态 IoC 类作为服务定位器:
// 旧代码:静态 IoC 容器包装
public static class IoC
{
private static IDependencyResolver? _resolver;
public static void Initialize(IDependencyResolver resolver)
=> _resolver = resolver;
public static T Resolve<T>() where T : class
=> _resolver?.Resolve<T>()
?? throw new InvalidOperationException("IoC 未初始化");
public static void RegisterType<TFrom, TTo>(LifeTime lifetime = LifeTime.PerCall)
=> _resolver?.RegisterType<TFrom, TTo>(lifetime);
public static void RegisterInstance<TInterface>(TInterface instance)
where TInterface : class
=> _resolver?.RegisterInstance(instance);
}
// 调用方:服务定位器模式
public class SparePartController : Controller
{
private readonly ISparePartService _service = IoC.Resolve<ISparePartService>();
public ActionResult StockIn(SparePartDto dto)
{
_service.StockIn(dto);
return Json(new { success = true });
}
}
问题:
- 依赖关系隐藏——看类的构造函数不知道它需要什么
- 单元测试困难——必须先初始化静态容器
- 静态状态在测试间泄漏
- 生命周期管理混乱(
PerCall/Singleton/PerResolve/PerThread/External五种生命周期难以追踪)
8.2 迁移到 ASP.NET Core DI
// ✅ 迁移后:构造函数注入,依赖一目了然
public class SparePartController : ControllerBase
{
private readonly ISparePartService _service;
public SparePartController(ISparePartService service)
{
_service = service;
}
[HttpPost]
public async Task<IActionResult> StockIn(
SparePartDto dto, CancellationToken ct)
{
await _service.StockInAsync(dto, ct);
return Ok();
}
}
8.3 兼容旧代码的过渡方案
迁移过程中,仍有大量旧代码调用 IoC.Resolve<T>()。为了不一次性重写所有代码,可以做一个适配器:
// 旧接口适配器:让静态 IoC 包装新的 IServiceProvider
public class ServiceProviderDependencyResolver : IDependencyResolver
{
private readonly IServiceProvider _serviceProvider;
public ServiceProviderDependencyResolver(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public T Resolve<T>() where T : class
=> _serviceProvider.GetRequiredService<T>();
public void RegisterType<TFrom, TTo>(LifeTime lifetime)
{
// ASP.NET Core 容器在 Build 后不支持动态注册
// 这部分需要在 Startup/Program 中统一注册
throw new NotSupportedException(
"请在 Program.cs 中使用 IServiceCollection 注册服务");
}
}
// 在 Program.cs 中初始化
var app = builder.Build();
IoC.Initialize(new ServiceProviderDependencyResolver(app.Services));
这样旧代码不需要改动就能继续工作,新代码全部用构造函数注入,逐步迁移。
8.4 生命周期映射
旧系统到新系统的生命周期映射:
| 旧 IoC LifeTime | 新 DI ServiceLifetime | 说明 |
|---|---|---|
PerCall | Transient | 每次解析创建新实例 |
Singleton | Singleton | 全局唯一 |
PerResolve | Scoped(近似) | 每次解析图中共享 |
PerThread | 无对应 | ASP.NET Core 不推荐线程本地存储 |
External | 外部实例传入 | RegisterInstance 或工厂 |
9. 注册策略与程序集扫描
9.1 手动注册 vs 程序集扫描
小型项目手动注册没问题:
builder.Services.AddScoped<ISparePartService, SparePartService>();
builder.Services.AddScoped<IEquipmentService, EquipmentService>();
builder.Services.AddScoped<IPurchaseOrderService, PurchaseOrderService>();
// ... 100 个服务写 100 行
大型项目用程序集扫描自动注册:
// 约定:所有实现 ITransient 接口的类注册为 Transient
// 所有实现 IScoped 接口的类注册为 Scoped
// 所有实现 ISingleton 接口的类注册为 Singleton
public interface IScoped { }
public interface ITransient { }
public interface ISingleton { }
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddAssemblyServices(
this IServiceCollection services,
params Assembly[] assemblies)
{
var types = assemblies.SelectMany(a => a.GetTypes())
.Where(t => !t.IsAbstract && !t.IsInterface && !t.IsGenericTypeDefinition)
.ToList();
foreach (var type in types)
{
var lifetime = GetLifetime(type);
if (lifetime == null) continue;
// 注册该类实现的所有接口
var interfaces = type.GetInterfaces()
.Where(i => i != typeof(IScoped) &&
i != typeof(ITransient) &&
i != typeof(ISingleton));
foreach (var @interface in interfaces)
{
var descriptor = new ServiceDescriptor(
@interface, type, lifetime.Value);
services.Add(descriptor);
}
// 同时注册具体类型自身
services.Add(new ServiceDescriptor(type, type, lifetime.Value));
}
return services;
}
private static ServiceLifetime? GetLifetime(Type type)
{
if (typeof(ISingleton).IsAssignableFrom(type))
return ServiceLifetime.Singleton;
if (typeof(IScoped).IsAssignableFrom(type))
return ServiceLifetime.Scoped;
if (typeof(ITransient).IsAssignableFrom(type))
return ServiceLifetime.Transient;
return null;
}
}
// 使用
builder.Services.AddAssemblyServices(
typeof(Program).Assembly, // API 层
typeof(SparePartService).Assembly, // Application 层
typeof(EfRepository<>).Assembly); // Infrastructure 层
9.2 标记接口方式
// Service 层标记
public class SparePartService : ISparePartService, IScoped
{
// 自动注册为 Scoped 的 ISparePartService
}
public class CacheService : ICacheService, ISingleton
{
// 自动注册为 Singleton 的 ICacheService
}
public class EmailSender : IEmailSender, ITransient
{
// 自动注册为 Transient 的 IEmailSender
}
优点:
- 新增服务只需加个标记接口,不用修改注册代码
- 服务的生命周期在类自身上声明,一目了然
- 避免遗漏注册
注意:
- 标记接口是"横切关注点",不要和领域接口混淆
- 可以用特性(Attribute)替代:
[Service(Lifetime = ServiceLifetime.Scoped)]
9.3 仓储的批量注册
// 泛型仓储注册
builder.Services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>));
// 特定仓储的自定义实现
builder.Services.AddScoped<ISparePartRepository, SparePartRepository>();
当解析 IRepository<SparePart> 时:
- 先查找是否有精确注册
IRepository<SparePart>→ 有就用 - 没有则匹配开放泛型
IRepository<>→EfRepository<SparePart>
10. Keyed Services:同一接口多实现
10.1 基本用法
.NET 8 引入了 Keyed Services(之前叫 Keyed Services,.NET 7 中是 ServicesKeyedService),用于同一接口有多个实现的场景:
// 注册
builder.Services.AddKeyedSingleton<INotifier, EmailNotifier>("email");
builder.Services.AddKeyedSingleton<INotifier, SmsNotifier>("sms");
builder.Services.AddKeyedSingleton<INotifier, WeChatNotifier>("wechat");
// 解析
public class NotificationService
{
private readonly INotifier _emailNotifier;
private readonly INotifier _smsNotifier;
public NotificationService(
[FromKeyedServices("email")] INotifier emailNotifier,
[FromKeyedServices("sms")] INotifier smsNotifier)
{
_emailNotifier = emailNotifier;
_smsNotifier = smsNotifier;
}
}
10.2 PMS 场景:多船舶数据源
不同船舶可能使用不同的数据库连接(SQL Server / SQLite / 远程 API),用 Keyed Services 实现数据源路由:
public interface IShipDataSource
{
Task<List<SparePart>> GetSparePartsAsync(long shipId, CancellationToken ct);
}
public class SqlServerShipDataSource : IShipDataSource { /* ... */ }
public class SqliteShipDataSource : IShipDataSource { /* ... */ }
public class RemoteApiShipDataSource : IShipDataSource { /* ... */ }
// 注册
builder.Services.AddKeyedScoped<IShipDataSource, SqlServerShipDataSource>("sqlserver");
builder.Services.AddKeyedScoped<IShipDataSource, SqliteShipDataSource>("sqlite");
builder.Services.AddKeyedScoped<IShipDataSource, RemoteApiShipDataSource>("remote");
// 路由器
public class ShipDataSourceRouter : IShipDataSourceRouter
{
private readonly IServiceProvider _sp;
private readonly IShipConfigService _shipConfig;
public ShipDataSourceRouter(
IServiceProvider sp,
IShipConfigService shipConfig)
{
_sp = sp;
_shipConfig = shipConfig;
}
public IShipDataSource Resolve(long shipId)
{
var sourceType = _shipConfig.GetDataSourceType(shipId);
return _sp.GetRequiredKeyedService<IShipDataSource>(sourceType);
}
}
10.3 枚举所有 Keyed 服务
public class NotificationDispatcher
{
private readonly IEnumerable<KeyValuePair<string, INotifier>> _notifiers;
public NotificationDispatcher(
IKeyedServiceProvider keyedProvider,
IEnumerable<INotifier> allNotifiers)
{
// 注入所有 INotifier(非 keyed)
}
// 或者通过 IServiceProviderIsKeyedService 判断
public void Dispatch(string channel, string title, string message)
{
// 按 channel 解析
}
}
10.4 .NET 8 之前的替代方案
如果还在用 .NET 6,可以用工厂模式:
public delegate INotifier NotifierFactory(string channel);
builder.Services.AddSingleton<EmailNotifier>();
builder.Services.AddSingleton<SmsNotifier>();
builder.Services.AddSingleton<NotifierFactory>(sp => channel =>
channel switch
{
"email" => sp.GetRequiredService<EmailNotifier>(),
"sms" => sp.GetRequiredService<SmsNotifier>(),
_ => throw new KeyNotFoundException($"不支持的通知渠道: {channel}")
});
11. 泛型服务与开放泛型
11.1 开放泛型注册
// 注册开放泛型(不带具体类型参数)
builder.Services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>));
builder.Services.AddScoped(typeof(IValidator<>), typeof(DataAnnotationValidator<>));
// 解析时自动构造闭合泛型
// IRepository<SparePart> → EfRepository<SparePart>
// IRepository<Equipment> → EfRepository<Equipment>
11.2 泛型特性约束
// 仓储只接受继承自 AggregateRoot 的类型
public class EfRepository<T> : IRepository<T> where T : AggregateRoot
{
// ...
}
// 如果解析 IRepository<SomeDto>(SomeDto 不是 AggregateRoot)
// 容器会尝试找其他注册,找不到则抛异常
11.3 泛型服务的装饰器模式
.NET Core DI 原生不支持装饰器,但可以用工厂实现:
builder.Services.AddScoped<EfRepository<SparePart>>();
builder.Services.AddScoped<IRepository<SparePart>>(sp =>
new CachingRepository<SparePart>(
sp.GetRequiredService<EfRepository<SparePart>>(),
sp.GetRequiredService<ICacheService>()));
如果用了 Autofac,装饰器是内置支持的。
12. ActivatorUtilities 与工厂模式
12.1 ActivatorUtilities.CreateInstance
当你需要创建一个未注册的类型,但希望它的构造函数参数从容器中解析:
// 这个类没有在容器中注册,但它的依赖需要从容器获取
public class ReportExporter
{
public ReportExporter(
PmsDbContext dbContext,
ILogger<ReportExporter> logger,
string reportType) // 这个参数容器不知道
{
// ...
}
}
// 使用 ActivatorUtilities 注入容器能解析的参数,手动提供其他参数
var exporter = ActivatorUtilities.CreateInstance<ReportExporter>(
serviceProvider,
"monthly"); // reportType 参数
12.2 工厂注册
builder.Services.AddScoped<ISparePartService>(sp =>
{
var dbContext = sp.GetRequiredService<PmsDbContext>();
var logger = sp.GetRequiredService<ILogger<SparePartService>>();
// 根据配置选择不同实现
var useCaching = sp.GetRequiredService<IConfiguration>()
.GetValue<bool>("Features:SparePartCaching");
if (useCaching)
return new CachedSparePartService(dbContext, logger,
sp.GetRequiredService<ICacheService>());
return new SparePartService(dbContext, logger);
});
12.3 条件注册
// 仅在特定环境注册
if (builder.Environment.IsDevelopment())
{
builder.Services.AddScoped<IEmailSender, FakeEmailSender>();
}
else
{
builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();
}
// 替换已有注册(最后注册的生效)
builder.Services.AddScoped<INotifier, EmailNotifier>();
if (builder.Configuration.GetValue<bool>("Features:SmsNotification"))
{
// 替换:先移除再添加,或直接 Add 覆盖
var descriptor = builder.Services.FirstOrDefault(
d => d.ServiceType == typeof(INotifier));
if (descriptor != null)
builder.Services.Remove(descriptor);
builder.Services.AddScoped<INotifier, CompositeNotifier>();
}
13. 第三方容器:Autofac / DryIoc 集成
13.1 什么时候需要第三方容器
.NET Core 自带的 DI 容器有意保持简单,不支持:
- 属性注入
- 装饰器模式(原生支持有限)
- 子容器/嵌套生命周期
- 动态代理/AOP
- 程序集扫描的高级约定
- 命名服务(.NET 8 的 Keyed Services 填补了这个空白)
如果你的项目用了 AOP(如 AOP 实战那篇中的拦截器),Autofac 是最常见的选择。
13.2 Autofac 集成
dotnet add package Autofac.Extensions.DependencyInjection
// Program.cs
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
builder.Host.ConfigureContainer<ContainerBuilder>(containerBuilder =>
{
// 程序集扫描
containerBuilder.RegisterAssemblyTypes(typeof(SparePartService).Assembly)
.Where(t => t.Name.EndsWith("Service"))
.AsImplementedInterfaces()
.InstancePerLifetimeScope();
// 属性注入
containerBuilder.RegisterType<SparePartController>()
.PropertiesAutowired();
// 装饰器
containerBuilder.RegisterType<EfRepository<SparePart>>()
.As<IRepository<SparePart>>();
containerBuilder.RegisterDecorator<CachingRepository<SparePart>, IRepository<SparePart>>();
// AOP 拦截器
containerBuilder.RegisterType<TransactionInterceptor>();
containerBuilder.RegisterType<SparePartService>()
.As<ISparePartService>()
.EnableInterfaceInterceptors()
.InterceptedBy(typeof(TransactionInterceptor));
});
13.3 注意事项
- 第三方容器会覆盖 .NET Core 的 DI 行为,部分诊断工具可能失效
- 从 .NET 8 开始,Keyed Services 原生支持,很多场景不再需要 Autofac
- AOP 拦截器有一定性能开销(动态代理),不要在高频核心路径上滥用
14. DI 验证与诊断工具
14.1 启动验证
builder.WebHost.UseDefaultServiceProvider((context, options) =>
{
options.ValidateScopes = true; // 验证 Captive Dependency
options.ValidateOnBuild = true; // Build 时验证所有服务能解析
});
生产环境建议:
ValidateScopes= false(有性能开销)ValidateOnBuild= false(启动慢)- 在测试环境开启,CI 中跑一次启动测试
14.2 常见异常及解决
| 异常 | 原因 | 解决 |
|---|---|---|
InvalidOperationException: Unable to resolve service | 未注册或生命周期不对 | 在 Program.cs 注册对应服务 |
InvalidOperationException: Cannot consume scoped service from singleton | Captive Dependency | 用 IServiceScopeFactory 或改为方法注入 |
InvalidOperationException: A second operation was started | DbContext 并发 | 检查是否被 Singleton 持有,或是否跨线程共享 |
ObjectDisposedException | 使用了已释放的 Scope | 检查异步操作是否超出了请求生命周期 |
Circular dependency detected | 循环依赖 | 重构代码,引入中介者或事件总线解耦 |
14.3 诊断工具
# 查看已注册的服务
dotnet tool install -g dotnet-dump
# 或在代码中列出所有注册
var allServices = app.Services.GetServices<ServiceDescriptor>();
foreach (var s in allServices)
{
Console.WriteLine($"{s.ServiceType.Name} -> " +
$"{s.ImplementationType?.Name ?? "instance/factory"} " +
$"({s.Lifetime})");
}
14.4 单元测试中的 DI
// 用独立的 ServiceCollection 做测试
public class SparePartServiceTests
{
[Fact]
public async Task StockIn_Should_Save_And_Notify()
{
// Arrange
var services = new ServiceCollection();
services.AddScoped<ISparePartService, SparePartService>();
services.AddScoped<IRepository<SparePart>>(_ =>
new Mock<IRepository<SparePart>>().Object);
services.AddScoped<INotifier>(_ =>
new Mock<INotifier>().Object);
services.AddLogging();
var provider = services.BuildServiceProvider();
using var scope = provider.CreateScope();
var service = scope.ServiceProvider.GetRequiredService<ISparePartService>();
// Act
await service.StockInAsync(new SparePartDto { /* ... */ });
// Assert
// ...
}
}
更简单的方式——单元测试通常不需要真正的容器,直接 new 就行:
[Fact]
public async Task StockIn_Should_Save_And_Notify()
{
var mockRepo = new Mock<IRepository<SparePart>>();
var mockNotifier = new Mock<INotifier>();
var logger = new TestLogger<SparePartService>(); // 测试用的假 Logger
var service = new SparePartService(
mockRepo.Object,
mockNotifier.Object,
logger);
await service.StockInAsync(new SparePartDto());
mockRepo.Verify(r => r.AddAsync(It.IsAny<SparePart>(), It.IsAny<CancellationToken>()),
Times.Once);
}
15. Checklist
注册
- 必选依赖用构造函数注入,声明为
readonly - DbContext、Repository、UnitOfWork 注册为 Scoped
- 无状态服务(缓存、配置、工厂)注册为 Singleton
- 轻量无状态服务注册为 Transient
- 开放泛型用
AddScoped(typeof(IRepository<>), typeof(EfRepository<>)) - 同一接口多实现用 Keyed Services(.NET 8+)或工厂委托
- 大型项目用程序集扫描自动注册,避免遗漏
生命周期
- Singleton 不依赖 Scoped 或 Transient(Captive Dependency)
- 中间件的 Scoped 依赖通过
InvokeAsync参数注入 - 后台任务通过
IServiceScopeFactory创建 Scope - 批量处理每批创建独立 Scope,避免 Change Tracker 膨胀
- 开发环境开启
ValidateScopes和ValidateOnBuild - Transient 的
IDisposable对象注意泄漏风险
安全与可测试
- 不使用服务定位器模式(
IoC.Resolve<T>()),用构造函数注入 - 不注入
IServiceProvider本身(那是服务定位器) - 构造函数参数不超过 5 个,过多说明职责过重
- 单元测试直接 new,不需要真正的容器
- 不在容器中注册实体/DTO 等数据对象
PMS 项目专项
- 旧
IoC.Resolve<T>()代码逐步迁移到构造函数注入 - 旧生命周期映射:PerCall→Transient、Singleton→Singleton
- 多船舶数据源用 Keyed Services 路由
- 仓储用开放泛型注册,特殊仓储单独注册
- 拦截器/装饰器考虑用 Autofac 或第三方容器
总结
IoC/DI 是 .NET Core 应用的骨架,理解它的三个关键问题:
- 谁来创建对象? ——容器创建,你只声明依赖
- 对象活多久? ——Singleton / Scoped / Transient,按场景选择
- 依赖能正确解析吗? ——注意 Captive Dependency、循环依赖、生命周期约束
记住一个核心原则:依赖应该向内流入(构造函数注入),而不是向外拉取(服务定位器)。当你的类只通过构造函数声明需要什么,而不关心这些东西从哪里来,你就真正掌握了 IoC。
💬 最后互动:你的项目中 DI 容器用的是原生 .NET Core DI 还是 Autofac?有没有遇到过特别难排查的 DI 相关问题?我先来:曾经因为一个 Singleton 的
IMemoryCache内部缓存了 Scoped 仓储的查询结果,缓存的实体被多个请求的 Change Tracker 同时跟踪,间歇性出现"实体已在另一个上下文中跟踪"异常。这个 bug 排查了两天,因为它只在特定缓存命中时才出现。评论区聊聊你的故事。

202

被折叠的 条评论
为什么被折叠?



