Context

Context 把根级服务注册表、根级事件总线和当前 effect 作用域组合在一起。 本文以 Tether 当前公开 API 为主,并逐项对照 dsh 固定基线中的 TypeScript Context。

对照基准是 dsh dsh-v0.1.1-rc.1(commit 528c682e)及其 Context 参考页。 C# 对照的 TS 源码是固定提交中的 context.ts 与 reflect.ts。 C# 实现见 Context.cs 与 Registry.cs。

如何读对照表:对应表示调用方可观察语义一致;近似表示承担相同职责但边界不同; TS-only 只表示 C# 没有同层公开 API,不自动等于 parity 缺陷。

TS → C# 逐项对照

TypeScriptC#状态与差异
ctx.extend(meta?)无直接对应;生命周期子上下文用 ctx.Fork()近似。Fork() 只创建独立 effect scope,不提供原型继承或任意 metadata shadow。
ctx.isolate(name, label?)无TS-only。C# 服务键在根 Registry 中共享,当前没有按 label 隔离同一键的公开层。
ctx.intercept(name, config)无TS-only。C# 不提供 Context 级服务配置拦截链。
ctx.rootctx.Root对应。所有 fork 指向同一根 Context。
ctx.baseUrl无 Context 属性TS-only。Loader、脚本编译器和 HMR 显式接收路径或目录。
ctx.eventsctx.Events对应。整棵 Context 树共享一个 EventService。
ctx.logger无 Cordis 核心服务TS-only。日志由宿主或产品层显式注入,不由 Context proxy 提供。
ctx.reflect无TS-only。C# 使用显式泛型方法和 Registry,没有代理反射层。
ctx.registryctx.Registry近似。C# Registry 同时是 Type-keyed 服务仓库和 inject 激活图。
Context.effect无静态 symbolTS-only 语法设施。C# 生命周期所有权由 EffectScope 类型表达。
Context.filter无TS-only。C# 监听器没有 Context filter / global 旁路。
Context.isolate无静态 symbolTS-only 语法设施。
Context.intercept无静态 symbolTS-only 语法设施。
Context.is(value)value is Context语言对应。C# 类型系统不需要跨 realm 的 global-symbol brand。
ctx.get(name, strict?)Get<T>();Registry.Get(Type)近似。键从字符串变为 Type;C# 只暴露已发布的权威值,没有 strict 参数。
ctx.set(name, value)SetAsync<T>(value);SetAsync(Type, object)对应且可等待。只有 exact providing scope 可更新;依赖插件完成重启后才结束。
ctx.provide(name, value)Provide<T>(value);Provide(Type, object)对应且更强生命周期契约。返回 ServiceRegistration,初次依赖收敛通过 Completion 观察。
ctx.accessor(name, options)无TS-only。C# 不在 Context 上动态声明计算属性。
ctx.mixin(name, mixins)无TS-only。服务通过接口和 Get<T>() / Require<T>() 访问,不把成员动态混入 Context。

C# Context 公共接口面

创建、根与环境句柄

public Context();

public Context Root { get; }
public bool IsRoot { get; }
public Registry Registry { get; }
public EventService Events { get; }
public EffectScope Scope { get; }
public bool IsReady { get; }

new Context() 创建根。fork 共享 Root.Registry 和 Root.Events,但各自拥有 Scope。 因此服务与事件是整棵树的公共通信面,注册和清理所有权则停留在发起调用的 scope。

生命周期

public ValueTask StartAsync();
public ValueTask DisposeAsync();
  • StartAsync() 只能在根 Context 调用;首次调用把 IsReady 设为 true,再顺序派发 ready。
  • 后续 StartAsync() 是 no-op。ready 监听器失败会传播给首次调用方,但不会把根退回未启动状态。
  • DisposeAsync() 回收该 Context 的 scope 子树;在根上调用才是整棵 Context 树。它先取消 token、 等待 quiescence、逆序回收子 scope,派发 dispose,最后 LIFO 运行当前 scope 的 disposer;全部清理失败会聚合。

TS Context 本身没有这组同名根启动 API;dsh 的启动时序由 loader / fiber 管理。C# 把宿主启动边界显式放在根 Context 上。

Fork

public Context Fork();

Fork() 创建子 EffectScope 并挂到当前 scope。父 scope 开始卸载后,新的 fork 会抛 ObjectDisposedException。创建成功后会以 contained 方式派发 fork:调用不等待监听器,失败送往 internal/error 或 trace,不会让同步 Fork() 失败。

它对应 TS ctx.extend() 的“派生子上下文”用途,但没有 metadata、service isolation 或 intercept 能力。

读取服务

public T? Get<T>() where T : class;
public T Require<T>() where T : class;
public bool Has<T>();

public object? Registry.Get(Type key);
public bool Registry.Has(Type key);
public IReadOnlyCollection<Type> Registry.Keys { get; }
  • Get<T>() 在服务尚未发布或不存在时返回 null。
  • Require<T>() 在缺失时抛 ServiceNotFoundException,异常的 ServiceType 保留缺失键。
  • Has<T>() 与 Registry.Keys 只观察已发布服务;插件 apply 中的预留发布不可见。

TS 使用字符串属性名并由 Context proxy 解析;C# 使用 Type,通常是独立 Service Definition 程序集里的接口。

提供、更新与移除服务

public ServiceRegistration Provide<T>(T instance) where T : class;
public ServiceRegistration Provide(Type key, object instance);

public ValueTask SetAsync<T>(T instance) where T : class;
public ValueTask SetAsync(Type key, object instance);

public ValueTask<bool> RemoveAsync<T>();
public ValueTask<bool> RemoveAsync(Type key);

注册属于调用 Context 的 exact scope:

  • 同一键已有提供者时,Provide 抛 InvalidOperationException;
  • 实例不能赋值给键类型时,抛 ArgumentException;
  • 只有 exact providing scope 可以 SetAsync 或 RemoveAsync,其他 fork 立即失败且不改变权威值;
  • SetAsync 更换实例会重启依赖该键的 active / failed 插件,并等待目标代次收敛;
  • SetAsync 传入与当前权威值引用相同的实例时仍派发 service 收敛通知,但不会重启依赖插件;
  • RemoveAsync 等待依赖插件完成清理;键不存在时返回 false;
  • 插件 apply 内的多次 Provide 成批预留,只有 apply 成功后才一起发布。

ServiceRegistration 的完整接口见 Plugin Registry。

注册 disposer

public void OnDispose(Action disposer);
public void OnDispose(Func<ValueTask> disposer);
public void Effect(Action disposer);
public void Effect(Func<ValueTask> disposer);

四个重载都把已经取得的清理动作登记到当前 EffectScope。这点与 TS ctx.effect(execute) 不同: TS 先执行 execute,再收集它返回或 yield 的 disposer;C# Effect 本身不执行资源工厂,也不返回提前清理句柄。 需要可提前摘除的注册时,使用 ctx.On(...) 返回的 IDisposable、ServiceRegistration,或直接调用 ctx.Scope.OnDispose(...) 取得登记句柄。

其余混入 API 的 C# 位置

Context 还显式转发下列 API;详情放在各自页面,避免在本页重复全部泛型重载:

类别C# 成员参考页
事件订阅On、OnWaterfall、Once、OffEvents
事件派发EmitAsync、ParallelAsync、SerialAsync、BailAsync、WaterfallAsyncEvents
插件挂载Plugin、Plugin<TPlugin>、InjectPlugin Registry

最小示例

await using var registration = root.Provide<IClock>(new SystemClock());

var child = root.Fork();
child.On<string>("audit", message => Console.WriteLine(message));

IClock clock = child.Require<IClock>();
await child.EmitAsync("audit", clock.UtcNow.ToString("O"));

await child.DisposeAsync();       // 摘除 audit listener,不移除根提供的 IClock
await registration.DisposeAsync();

这里的生命周期边界无法通过 TS 的属性访问外观直接看出,但在 C# 类型上是显式的:服务注册归提供 scope, 监听器归订阅 scope,fork 只共享根仓库和事件总线。

TS-only 接口为什么没有同名移植

TS Context 同时承担动态对象代理、服务名空间、声明合并和 scope metadata。C# 版把这些职责拆开:

  • 编译期服务 seam 用接口 Type,替代字符串属性与 mixin();
  • Registry 负责服务权威值和 inject 激活图,替代 reflect proxy;
  • EffectScope 表达生命周期,替代 symbol-keyed effect metadata;
  • Loader / HMR 显式对象承担路径、组合和热重载,替代 ambient baseUrl / loader / hmr 属性。

因此 extend、isolate、intercept、accessor、mixin 和静态 symbol 没有伪造一组同名空壳; 需要这些语义时,应先确认它是否属于当前 C# 调用方可观察需求,再在相应 seam 上设计。

在 GitHub 上编辑此页