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# 逐项对照
| TypeScript | C# | 状态与差异 |
|---|---|---|
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.Function | Plugin.From(delegate);直接实现 IPlugin | 对应。 |
Plugin.Constructor | Plugin / IPlugin 类 + Plugin<TPlugin>() | 近似。泛型快捷方式要求公开无参构造;有构造参数时先创建实例再传给 Plugin()。 |
Plugin.Object.apply | IPlugin.ApplyAsync(Context) | 对应。 |
Plugin.Base.name | 无元数据;可看 Plugin.GetType() | TS-only。C# 核心不维护 display name。 |
Plugin.Base.Config | Loader 的配置类型 + DataAnnotations;IConfigurablePlugin | 职责拆分。核心只传递已绑定对象,schema 校验属于 Loader。 |
Plugin.Base.inject | IPlugin.Inject;[Inject(...)] | 对应。键从字符串变为 Type。 |
Plugin.Base.provide | ctx.Provide<T>() | 近似。C# 不靠静态 metadata 声明提供键,apply 中的真实注册才是权威。 |
Plugin.Base.intercept | 无 | TS-only。C# 没有服务 intercept config。 |
Plugin.Transform | 无核心类型 | TS-only。Loader 直接把 YAML 绑定为配置类型。 |
Plugin.Runtime | PluginHandle(部分职责) | 近似。C# 每次挂载一个句柄,不公开“同一 callback 的全部 fibers”全局记录。 |
Inject array | IReadOnlyList<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()、服务收敛或组合加载边界观察它。