Service

CordisService 是长期服务的生命周期基类:应用 ready 时启动,所属 scope 回收时停止。 C# 把“生命周期”和“以哪个键发布”分开;服务通常按接口 Type 显式注册。

对照基准是 dsh dsh-v0.1.1-rc.1 的 Service 参考页 与 service.ts。 C# 实现见 Service.cs。

TS → C# 逐项对照

TypeScriptC#状态与差异
class Serviceabstract class CordisService近似。都把实例生命周期绑到 Context;C# 基类本身不自动选择服务键。
super(ctx, name)base(ctx) + ctx.Provide<IContract>(this)职责拆分。键从字符串 name 变为接口 Type。
service.name无TS-only。C# 注册身份在 ServiceRegistration / Registry 键上,不存为 service 字符串属性。
Service.initStartAsync()(近似)近似。TS symbol 是 class plugin 构造后的 hook;C# start 由根 ready 驱动。
Service.check无 hookTS-only。已发布注册即为 Registry 可用;额外 readiness 由服务契约自己表达。
Service.config无 phantom symbolTS-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。
在 GitHub 上编辑此页