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# 逐项对照
| TypeScript | C# | 状态与差异 |
|---|---|---|
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.root | ctx.Root | 对应。所有 fork 指向同一根 Context。 |
ctx.baseUrl | 无 Context 属性 | TS-only。Loader、脚本编译器和 HMR 显式接收路径或目录。 |
ctx.events | ctx.Events | 对应。整棵 Context 树共享一个 EventService。 |
ctx.logger | 无 Cordis 核心服务 | TS-only。日志由宿主或产品层显式注入,不由 Context proxy 提供。 |
ctx.reflect | 无 | TS-only。C# 使用显式泛型方法和 Registry,没有代理反射层。 |
ctx.registry | ctx.Registry | 近似。C# Registry 同时是 Type-keyed 服务仓库和 inject 激活图。 |
Context.effect | 无静态 symbol | TS-only 语法设施。C# 生命周期所有权由 EffectScope 类型表达。 |
Context.filter | 无 | TS-only。C# 监听器没有 Context filter / global 旁路。 |
Context.isolate | 无静态 symbol | TS-only 语法设施。 |
Context.intercept | 无静态 symbol | TS-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、Off | Events |
| 事件派发 | EmitAsync、ParallelAsync、SerialAsync、BailAsync、WaterfallAsync | Events |
| 插件挂载 | Plugin、Plugin<TPlugin>、Inject | Plugin 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 激活图,替代reflectproxy;EffectScope表达生命周期,替代 symbol-keyed effect metadata;- Loader / HMR 显式对象承担路径、组合和热重载,替代 ambient
baseUrl/loader/hmr属性。
因此 extend、isolate、intercept、accessor、mixin 和静态 symbol 没有伪造一组同名空壳;
需要这些语义时,应先确认它是否属于当前 C# 调用方可观察需求,再在相应 seam 上设计。