Service
CordisService 是长期服务的生命周期基类:应用 ready 时启动,所属 scope 回收时停止。
C# 把“生命周期”和“以哪个键发布”分开;服务通常按接口 Type 显式注册。
对照基准是 dsh dsh-v0.1.1-rc.1 的
Service 参考页 与
service.ts。
C# 实现见 Service.cs。
TS → C# 逐项对照
| TypeScript | C# | 状态与差异 |
|---|---|---|
class Service | abstract class CordisService | 近似。都把实例生命周期绑到 Context;C# 基类本身不自动选择服务键。 |
super(ctx, name) | base(ctx) + ctx.Provide<IContract>(this) | 职责拆分。键从字符串 name 变为接口 Type。 |
service.name | 无 | TS-only。C# 注册身份在 ServiceRegistration / Registry 键上,不存为 service 字符串属性。 |
Service.init | StartAsync()(近似) | 近似。TS symbol 是 class plugin 构造后的 hook;C# start 由根 ready 驱动。 |
Service.check | 无 hook | TS-only。已发布注册即为 Registry 可用;额外 readiness 由服务契约自己表达。 |
Service.config | 无 phantom symbol | TS-only 语法设施。配置使用普通 C# 类型。 |
Service.invoke | 无 | TS-only。C# 对象不可像函数一样调用,服务公开普通方法。 |
Service.extend | 普通继承 / 组合 | 语言对应,没有 Context proxy 专用 symbol。 |
Service.tracker | 无公开元数据 | TS-only。C# 核心不公开 context tracing tracker。 |
Service.resolveConfig | 无 | TS-only。C# 没有 ctx.intercept() 配置链。 |
CordisService
public abstract class CordisService
{
protected CordisService(Context context);
public Context Context { get; }
public virtual ValueTask StartAsync();
public virtual ValueTask StopAsync();
}
构造函数建立两条 scope-bound 生命周期注册:
- 根尚未 ready:通过
context.Once("ready", StartAsync)登记一次性启动;根StartAsync()会等待它; - 根已经 ready:立即发起
StartAsync();由于构造函数不能 await,失败以 contained 方式报告到internal/error,source 为service/start; - 无论哪种路径,都通过
context.OnDispose(StopAsync)登记停止动作。
StopAsync() 属于 scope cleanup,失败不会阻止其它 disposer 运行;最终由 EffectScope.DisposeAsync()
以 AggregateException 报告。直接 dispose CordisService 实例没有意义,它没有独立 DisposeAsync();
所有权在构造它的 Context scope。
CordisService 本身也不实现 IPlugin。常见做法是在 provider plugin 的 apply 中创建服务,使它与该 activation 同寿命。
CordisService<TSelf>
public abstract class CordisService<TSelf> : CordisService
where TSelf : CordisService<TSelf>
{
protected CordisService(Context context);
}
这个变体在构造时额外执行:
context.Provide<TSelf>((TSelf)(object)this);
因此它只按具体类型自动注册,适合契约本来就是具体服务类的情况:
public sealed class MetricsService(Context ctx)
: CordisService<MetricsService>(ctx)
{
public override ValueTask StartAsync() => ConnectAsync();
public override ValueTask StopAsync() => DisconnectAsync();
}
插件可随后 ctx.Require<MetricsService>()。泛型自注册同样采用 apply 期间预留发布、exact-scope owner 和依赖收敛规则。
推荐:按接口发布
能力 seam 通常定义在独立 Service Definition 程序集,因此更常见的写法是继承非泛型基类并显式提供接口:
public interface IClock
{
DateTimeOffset UtcNow { get; }
}
public sealed class SystemClockService : CordisService, IClock
{
public SystemClockService(Context ctx) : base(ctx)
=> ctx.Provide<IClock>(this);
public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
}
public sealed class ClockProviderPlugin : Plugin
{
protected override void Apply(Context ctx)
=> _ = new SystemClockService(ctx);
}
消费方只引用 IClock,组合 preset 可以替换 provider 而不改引用;服务实例、它的 StopAsync() 和接口注册都由
同一个 plugin activation scope 回收。
这与 TS ctx.<name> 的属性式访问不同:
IClock clock = ctx.Require<IClock>();
Console.WriteLine(clock.UtcNow);
发布与启动不是同一件事
CordisService 管启动 / 停止,Context.Provide 管 Registry 可见性:
- 在 plugin apply 内构造服务时,接口注册先预留,直到整个 apply 成功才公开;
- 根尚未 ready 时,服务可以已经发布,但
StartAsync()要等根启动; - 根已 ready 时,构造函数立即发起 start,服务发布仍遵循当前 activation 的事务边界;
ServiceRegistration.Completion观察依赖插件与service事件收敛,不代表StartAsync()的独立健康状态。
服务若必须“启动成功后才可见”,应由 provider plugin 先 await 初始化,再调用 Provide,而不是在
CordisService.StartAsync() 内假定 Registry 会延迟发布。
TS symbol hooks 的 .NET 对应方式
TS Service 依赖一组 unique symbol,在 Context proxy 上实现 callable service、intercept config 和 tracing。
C# 不需要复制这些元编程入口:
| TS symbol 用途 | C# 建议 |
|---|---|
| 构造后初始化 | 在 IPlugin.ApplyAsync 中显式 await,或用 CordisService.StartAsync 绑定根 ready。 |
| availability predicate | 在服务接口上公开健康状态,或只在初始化成功后 Provide。 |
| phantom config type | 普通 options / record 类型,Loader 用 DataAnnotations 校验。 |
| callable service | 接口方法,例如 logger.Create(name),不模拟函数对象。 |
| derive extended service | 普通继承、decorator 或组合。 |
| context tracker | 使用宿主日志 / telemetry seam,不在 Context 上隐藏元数据。 |
| intercept config resolution | 在 provider 或组合层显式合并;核心当前没有 intercept 链。 |
失败传播
| 失败点 | 调用方如何观察 |
|---|---|
根 ready 前的 StartAsync() | 作为 Context.StartAsync() 的普通 listener 失败直接传播。 |
根已 ready 后构造服务时的 StartAsync() | contained 到 internal/error(error, "service/start");没有 listener 时写 trace。 |
StopAsync() | scope 尽力完成全部清理,最终由 DisposeAsync() 聚合。 |
构造函数或 Provide | 使当前 plugin apply 失败,挂载方观察 PluginApplyException。 |
| 重复服务键 / 错误键类型 | Provide 立即抛 InvalidOperationException / ArgumentException。 |