iservicecollection用于服务注册,必须用接口类型作key;buildserviceprovider()构建iserviceprovider后才能解析服务;注册须在构建前完成,否则getservice抛nullreferenceexception。

别手写容器,IServiceCollection + BuildServiceProvider() 就够用;真要自己撸,核心就三件事:注册键必须是接口类型、Resolve 必须递归、单例缓存必须按接口类型存。
注册时为什么必须用接口类型作字典 Key
常见错误是把实现类当 key 存进映射表,比如写 _mappings[typeof(ConsoleLogger)] = typeof(ILogger),结果调用 Resolve<ilogger>()</ilogger> 时根本查不到——因为 key 是 ConsoleLogger 类型,不是 ILogger。
正确做法只有一条:注册时 typeof(TInterface) 是 key,typeof(TImplementation) 是 value。哪怕实现类没实现接口(编译会报错),也得确保字典里存的是接口类型。
实际影响:
- 接口未注册 →
Resolve<t>()</t>抛InvalidOperationException,而不是返回null;延迟暴露空引用风险 - 泛型接口如
ICommandHandler<createusercommand></createusercommand>,注册和解析必须完全一致,少一个尖括号或拼错字母都失败 - 若注册了
AddSingleton<myservice>()</myservice>(没带接口),后续只能通过GetService<myservice>()</myservice>拿,GetService<imyservice>()</imyservice>会找不到
Resolve 过程必须递归解析构造函数参数
Activator.CreateInstance 只能 new 无参类型。一旦你的类构造函数带依赖,比如 public UserService(ILogger logger, IDbContext db),就必须反射读取参数类型,再对每个参数类型调用一次 Resolve。
关键步骤:
- 用
type.GetConstructors()[0].GetParameters()拿到所有构造函数参数 - 对每个
ParameterInfo.ParameterType递归调用Resolve,形成依赖树 - 某依赖未注册,立刻抛异常;别等构造函数执行一半才崩
- 循环依赖(A→B→A)需加“正在构建中”集合检测,否则栈溢出
注意:IServiceProvider 默认只认 public 构造函数,且不支持可选参数或默认值;参数名写错(如 ILogger 写成 ILoger)编译不报错,运行时报 Unable to resolve service for type 'X'。
单例缓存必须按接口类型存,且线程不安全
单例不是“只注册一次”,而是“首次 Resolve 后把实例存起来,后续直接返回”。但缓存位置错了,等于白干。
正确做法:
- 存进
Dictionary<type object></type>,key 是接口类型(如typeof(ILogger)),不是实现类类型 - 否则
Resolve<ilogger>()</ilogger>和Resolve<iasynclogger>()</iasynclogger>拿不到同一个实例,哪怕它们指向同一实现 - 多线程下必须加锁或改用
ConcurrentDictionary,否则可能重复创建单例 -
AddSingleton<tinterface timplementation>()</tinterface>在 .NET 内置容器里是线程安全的,但手写容器不处理就大概率出问题
瞬时(Transient)不用缓存,每次走完整反射+new;作用域(Scoped)在控制台或 Worker Service 里必须手动 using var scope = provider.CreateScope(),否则退化为 Singleton,DbContext 等资源不会释放。
非 Web 场景下 Scoped 服务必须手动建作用域
.NET Web 项目自动为每个 HTTP 请求创建作用域,但控制台、Worker Service、单元测试里没有这个机制。直接 provider.GetRequiredService<irepository>()</irepository> 会报 No service for type 'IRepository' has been registered——不是没注册,是没作用域。
正确写法:
- 显式创建作用域:
using var scope = provider.CreateScope(); - 从
scope.ServiceProvider拿服务,不是从根provider - 记得
using,否则IDisposable实例(如DbContext)不会被及时释放 - 别把
scope.ServiceProvider存起来长期用——它只在当前作用域内有效
最容易被忽略的一点:AddSingleton 服务不能持有 Scoped 依赖,否则启动时就报 Cannot resolve scoped service from root provider。这不是配置顺序问题,是设计矛盾——单例生命周期长于作用域,容器拒绝这种非法组合。










