Plugin Registry

Registry 既保存 Type-keyed 服务,也维护插件的 pending / active / failed 激活图。 插件通过 Context.Plugin() 挂载,通过 Inject 声明何时可以 apply。

对照基准是 dsh dsh-v0.1.1-rc.1 的 Plugin Registry 参考页 与 registry.ts。 C# 实现见 Registry.cs、 Plugin.cs 和 PluginHandle.cs。

TS → C# 逐项对照

TypeScriptC#状态与差异
ctx.inject(deps, callback)ctx.Inject(Type[], callback)对应。依赖齐全时 apply;依赖实例变化时回收并重跑。C# 依赖只有 Type[],没有 intercept config map。
ctx.plugin(plugin, ...args)ctx.Plugin(IPlugin, config);ctx.Plugin<TPlugin>(config)对应。返回 PluginHandle;C# 句柄不是 thenable,使用 AwaitSettledAsync()。
Plugin.FunctionPlugin.From(delegate);直接实现 IPlugin对应。
Plugin.ConstructorPlugin / IPlugin 类 + Plugin<TPlugin>()近似。泛型快捷方式要求公开无参构造;有构造参数时先创建实例再传给 Plugin()。
Plugin.Object.applyIPlugin.ApplyAsync(Context)对应。
Plugin.Base.name无元数据;可看 Plugin.GetType()TS-only。C# 核心不维护 display name。
Plugin.Base.ConfigLoader 的配置类型 + DataAnnotations;IConfigurablePlugin职责拆分。核心只传递已绑定对象,schema 校验属于 Loader。
Plugin.Base.injectIPlugin.Inject;[Inject(...)]对应。键从字符串变为 Type。
Plugin.Base.providectx.Provide<T>()近似。C# 不靠静态 metadata 声明提供键,apply 中的真实注册才是权威。
Plugin.Base.intercept无TS-only。C# 没有服务 intercept config。
Plugin.Transform无核心类型TS-only。Loader 直接把 YAML 绑定为配置类型。
Plugin.RuntimePluginHandle(部分职责)近似。C# 每次挂载一个句柄,不公开“同一 callback 的全部 fibers”全局记录。
Inject arrayIReadOnlyList<Type> / Type[]对应。
Inject object map无TS-only。没有随依赖附带的 intercept 配置。
Inject.resolve(...)Plugin.Inject 聚合 attribute 并 Distinct()近似。没有公开 normalize helper。

挂载插件

public PluginHandle Context.Plugin(IPlugin plugin, object? config = null);

public PluginHandle Context.Plugin<TPlugin>(object? config = null)
    where TPlugin : class, IPlugin, new();

Plugin() 创建稳定的挂载句柄,并把它本身登记到当前父 scope。父 scope 卸载时句柄一定被 dispose。 方法返回不代表异步 apply 已稳定;调用方需要观察结果时使用:

PluginHandle handle = ctx.Plugin(new SearchPlugin(), config);
await handle.AwaitSettledAsync();

挂载后的状态由依赖决定:

条件Registry 行为
所有 Inject 键已发布创建 activation fork,先交付 config,再运行 ApplyAsync。
任一键缺失句柄留在 pending,不创建 activation scope。
pending 所缺键全部出现自动创建新代并 apply。
active / failed 依赖键被移除回收 activation scope,句柄退回 pending。
依赖键换成新实例回收旧代并 apply 新代,使插件重新绑定实例。
apply 失败回收该代 scope,句柄进入 failed;失败通过 PluginApplyException 可见。

pending 句柄在运行时快照中的 Missing 只列出当前仍未发布的依赖:部分依赖发布后,下一次服务收敛会把 Missing 收窄到剩余缺失项,因此快照与启动诊断不会继续等待一个已经发布的服务;全部依赖到齐时句柄直接 离开 pending 进入 apply。

每个代次的服务发布都是事务化的:apply 内 Provide 先预留;apply 成功后同批一起公开,失败则全部作废。 因此消费方不会看到只初始化了一半的提供者。

IPlugin

public interface IPlugin
{
    IReadOnlyList<Type> Inject => Array.Empty<Type>();
    ValueTask ApplyAsync(Context context);
}

ApplyAsync 总是在 Registry 创建的 fork Context 上运行。插件建立的 listener、service、disposer、child scope 和 child plugin 都必须通过该 Context 注册,才能随 activation 回收。

与 TS 函数 / class / object 三种结构联合不同,C# 用一个接口作为统一执行契约;便捷基类与 delegate factory 只负责减少样板代码。

IConfigurablePlugin

public interface IConfigurablePlugin
{
    void AcceptConfig(object? config);
}

若插件实现该接口,PluginHandle 在每次 activation 的 ApplyAsync 之前调用 AcceptConfig(Config)。 这里不校验 schema;通过 Cordis.Loader 挂载时,配置已经由 PluginCatalog / CompositionLoader 绑定为声明类型并用 DataAnnotations 校验。

AcceptConfig 或 ApplyAsync 失败都走同一 apply rollback,并通过 PluginApplyException 报告。

Plugin 便捷基类

public abstract class Plugin : IPlugin
{
    public virtual IReadOnlyList<Type> Inject { get; }
    protected abstract void Apply(Context context);

    public static IPlugin From(Action<Context> apply, params Type[] inject);
    public static IPlugin From(Func<Context, ValueTask> apply, params Type[] inject);
    public static IPlugin From<TConfig>(
        Action<Context, TConfig?> apply,
        params Type[] inject);
}

基类服务于同步插件:Apply(Context) 返回后视为 apply 完成。异步插件应直接实现 IPlugin.ApplyAsync, 或使用接收 Func<Context, ValueTask> 的 Plugin.From。

Plugin.From<TConfig> 生成同时实现 IConfigurablePlugin 的 delegate plugin,把挂载配置 cast 为 TConfig? 后传入回调;类型不匹配会在 activation 中失败,不会静默忽略。

[Inject]

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public sealed class InjectAttribute : Attribute
{
    public InjectAttribute(params Type[] services);
    public Type[] Services { get; }
}

Plugin 基类读取具体类型及其基类上的全部 attribute,合并并去重:

[Inject(typeof(IWorkspace))]
[Inject(typeof(ILlmClient), typeof(ISettings))]
public sealed class AgentPlugin : Plugin
{
    protected override void Apply(Context ctx)
    {
        var workspace = ctx.Require<IWorkspace>();
        var llm = ctx.Require<ILlmClient>();
        var settings = ctx.Require<ISettings>();
    }
}

直接实现 IPlugin 的类型不会自动读取 attribute,必须自己返回 Inject;或者继承 Plugin。

Context.Inject

public PluginHandle Inject(Type[] inject, Action<Context> apply);
public PluginHandle Inject(Type[] inject, Func<Context, ValueTask> apply);

这是 ctx.Plugin(Plugin.From(apply, inject)) 的快捷方式。与 TS 一样,callback 不是“依赖第一次出现时只跑一次”: 依赖消失会回收它,依赖重新出现或实例更新会用新 fork 重跑。

PluginHandle 摘要

public IPlugin Plugin { get; }
public object? Config { get; }
public bool IsActive { get; }
public Task Completion { get; }
public bool IsDisposed { get; }

public ValueTask<long> AwaitSettledAsync();
public ValueTask RestartAsync();
public void RequestRestart();
public ValueTask DisposeAsync();
public void RequestDispose();

PluginHandle 是 TS Fiber 的控制面对应物。Completion 观察一次 apply,AwaitSettledAsync 追踪到最新请求代次; RestartAsync 重新评估 inject,DisposeAsync 则 terminal unmount。完整生命周期见 Fiber。

Registry 公开读取面

Registry 的 mutation 入口刻意不公开;提供、更新和移除必须经过携带当前 scope 的 Context,才能执行 owner 检查。

public bool Has<T>();
public bool Has(Type key);
public T? Get<T>() where T : class;
public object? Get(Type key);
public T Require<T>() where T : class;
public IReadOnlyCollection<Type> Keys { get; }

这些成员只观察已发布的权威注册。TS Registry 参考页主要记录 plugin / inject mixin;C# 额外把只读服务仓库 作为明确对象公开,写操作仍通过 Context。

ServiceRegistration

ctx.Provide(...) 返回一个 scope-bound 注册对象,它是 TS provide() disposer 的可等待扩展。

public sealed class ServiceRegistration : IAsyncDisposable
{
    public Task Completion { get; }
    public ValueTask DisposeAsync();
    public void RequestDispose();
}

Completion

初次服务发布后,Registry 会并行推进受影响插件的 activation,并派发 service 事件;全部到达目标代次后 Completion 才结束。任一 apply 或事件 listener 失败时,它以聚合失败结束。

在插件 apply 中创建的注册只有等该 apply 成功返回、整批服务正式发布后才会 settle;同一个 apply 不得 await 自己的 Completion,否则它一边等待发布、一边阻止发布,形成确定死锁。

DisposeAsync() 与 RequestDispose()

DisposeAsync() 只移除该对象仍然代表的 exact registration,并等待所有受影响依赖方完成清理;重复调用共享结果。 scope disposal 始终是兜底,因此不显式 dispose 也不会跨插件卸载泄漏。

清理路径若已 yield,等待一个由自己清理触发的注册移除可能形成生命周期环,此时使用 RequestDispose() 只发起请求。

apply 失败可见性

public sealed class PluginApplyException : Exception
{
    public IPlugin Plugin { get; }
    public bool CleanupFailed { get; }
}

apply 失败后 Registry 会作废预留发布并回收 activation scope。若回收也失败,CleanupFailed 为 true, InnerException 保留 apply 与 cleanup 的聚合。失败不会被 internal/error 静默吞掉:挂载方通过 Completion、AwaitSettledAsync()、服务收敛或组合加载边界观察它。

在 GitHub 上编辑此页