服务与依赖注入
服务是插件之间唯一的耦合方式。注册表同时承担两个职责:存放服务实例,以及驱动插件的激活图。
对应实现:src/Cordis/Registry.cs。
服务以 Type 为键
服务键是一个 Type,通常是独立的 Service Definition 程序集里的接口——这样组合 preset 时只换实现不换引用。读取有三个入口:
IClock? maybe = ctx.Get<IClock>(); // 不存在返回 null
IClock clock = ctx.Require<IClock>(); // 不存在抛 ServiceNotFoundException
bool present = ctx.Has<IClock>(); // 只问在不在
ServiceNotFoundException 上带着 ServiceType,便于定位缺哪个键。ctx.Registry.Keys 给出当前已公开键的快照。
“存在”指的是已公开
Get、Require、Has 和 Keys 一律只看已公开的注册。
处在预留态的服务对它们不可见,具体见下文的预留发布一节。
提供服务
ctx.Provide<T>(instance) 注册一个由当前作用域拥有的服务,返回 ServiceRegistration。作用域回收时服务自动移除,所以正常情况下不需要手动登记清理。
public sealed class SystemClockPlugin : Plugin
{
protected override void Apply(Context ctx)
{
// 注册由 ctx.Scope 拥有,插件卸载时自动移除
ctx.Provide<IClock>(new SystemClock());
}
}
同一个键只能有一个权威提供者。几种失败是明确抛出的:
- 键已被别的提供者占用 →
InvalidOperationException; - 实例无法赋值给键类型 →
ArgumentException; - 对没提供过的键调
SetAsync→InvalidOperationException。
只有提供它的作用域能改
更新和移除都受 exact-scope 约束:只有当前持有该注册的那个作用域可以操作,别的作用域立即失败,且不会改动权威值。
await ctx.SetAsync<IClock>(new FakeClock()); // 替换实现,等依赖方收敛
bool existed = await ctx.RemoveAsync<IClock>(); // 移除,返回此前是否存在
RemoveAsync 在键本来就不存在时返回 false,不抛异常。非拥有者调用 SetAsync 或 RemoveAsync 会抛 InvalidOperationException。
同一个实例不会触发重启
SetAsync 用引用相等判断实例是否真的换了。传入与当前完全相同的实例时,
依赖它的插件不会被重启。
apply 期间的服务处于预留态
插件 apply 过程中 Provide 的服务不会立刻可见,而是先进入预留状态,等这次 apply 成功返回后整批一起公开。这样别的插件永远看不到一个只完成了一半的提供者。
- 预留期间
Has返回false、Get返回null; - apply 成功 → 该批全部转为公开,随后统一发出
service事件并驱动依赖方激活; - apply 失败 → 整批作废,注册的
Completion以说明未发布原因的异常失败。
不要在 apply 里 await 自己的注册
ServiceRegistration.Completion 只在提供它的那次 apply 成功返回之后才 settle。
在同一个 Apply 方法里 await 它会永久等待——这不是竞态,是确定的死锁。
inject 激活图
注册表把每个已挂载插件放在三种状态之一:等待依赖的 pending、已 apply 的 active、以及上次 apply 失败的 failed。服务的出现与消失驱动状态迁移。
SetAsync 换成新实例时,依赖它的 active 插件会重启以绑定新实例。| 发生了什么 | 激活图如何反应 |
|---|---|
插件挂载,Inject 已全部就位 | 立即在新 fork 上 apply,进入 active。 |
| 插件挂载,缺依赖 | 记下缺失的键,停在 pending。 |
| 某个键被公开 | 重新评估 pending,依赖齐了的插件被激活。 |
| 某个键被移除 | 依赖它的 active / failed 插件回收作用域,退回 pending。 |
| 某个键被换成新实例 | 依赖它的插件重启,以绑定新实例。 |
这些迁移都是可等待的。SetAsync、RemoveAsync 以及注册的公开过程都会等到受影响的插件到达目标代次;期间若有插件 apply 或清理失败,收敛会以 AggregateException 抛出,把每个失败都带上。
ServiceRegistration 的生命周期
Provide 返回的注册对象是精确控制单个服务生命周期的手段。
| 成员 | 语义 |
|---|---|
Completion | 初次注册的收敛信号:受影响的插件到达目标代次、service 事件跑完之后结束。 |
DisposeAsync() | 只移除这一个仍然权威的注册,并等待依赖方清理完成。重复调用返回同样的结果。 |
RequestDispose() | 发起移除但不等待,供已经让出的清理回调使用,避免生命周期环。 |
需要强调的是作用域回收始终是兜底:显式 dispose 注册只是提前移除的手段,忘了做也不会泄漏。