服务与依赖注入

服务是插件之间唯一的耦合方式。注册表同时承担两个职责:存放服务实例,以及驱动插件的激活图。 对应实现: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 成功返回后整批一起公开。这样别的插件永远看不到一个只完成了一半的提供者。

apply 返回的瞬间 apply 期间 ctx.Provide 登记服务 预留态:别人 Has=false、Get=null 成功 整批同时转为公开 再发 service 事件、驱动依赖方 失败 整批作废 Completion 以未发布原因失败 此期间没有"半个提供者"能被观察到
要么一起可见,要么一个都不可见。这条原子性保证了别的插件不会绑定到一个 apply 只完成了一半的提供者。
  • 预留期间 Has 返回 false、Get 返回 null;
  • apply 成功 → 该批全部转为公开,随后统一发出 service 事件并驱动依赖方激活;
  • apply 失败 → 整批作废,注册的 Completion 以说明未发布原因的异常失败。

不要在 apply 里 await 自己的注册

ServiceRegistration.Completion 只在提供它的那次 apply 成功返回之后才 settle。 在同一个 Apply 方法里 await 它会永久等待——这不是竞态,是确定的死锁。

inject 激活图

注册表把每个已挂载插件放在三种状态之一:等待依赖的 pending、已 apply 的 active、以及上次 apply 失败的 failed。服务的出现与消失驱动状态迁移。

依赖被移除,回收作用域并退回等待 挂载 pending 记下缺失的键 依赖齐了 active 已在新 fork 上 apply apply 失败 failed 可复活 RestartAsync 重新评估依赖
迁移由服务的出现与消失驱动,不由调用顺序决定。另有一条未画出的自环:提供者用 SetAsync 换成新实例时,依赖它的 active 插件会重启以绑定新实例。
发生了什么激活图如何反应
插件挂载,Inject 已全部就位立即在新 fork 上 apply,进入 active。
插件挂载,缺依赖记下缺失的键,停在 pending。
某个键被公开重新评估 pending,依赖齐了的插件被激活。
某个键被移除依赖它的 active / failed 插件回收作用域,退回 pending。
某个键被换成新实例依赖它的插件重启,以绑定新实例。

这些迁移都是可等待的。SetAsync、RemoveAsync 以及注册的公开过程都会等到受影响的插件到达目标代次;期间若有插件 apply 或清理失败,收敛会以 AggregateException 抛出,把每个失败都带上。

ServiceRegistration 的生命周期

Provide 返回的注册对象是精确控制单个服务生命周期的手段。

成员语义
Completion初次注册的收敛信号:受影响的插件到达目标代次、service 事件跑完之后结束。
DisposeAsync()只移除这一个仍然权威的注册,并等待依赖方清理完成。重复调用返回同样的结果。
RequestDispose()发起移除但不等待,供已经让出的清理回调使用,避免生命周期环。

需要强调的是作用域回收始终是兜底:显式 dispose 注册只是提前移除的手段,忘了做也不会泄漏。

下一步

在 GitHub 上编辑此页