1. 编写第一个插件

Cordis 插件是挂载到 Context 上的行为单元。Context.Plugin(plugin) 会用插件 fork 出的子 Context 调用它; 插件通过该 Context 注册自己贡献的所有内容。

编写插件

在 tmp/cordis-tutorial/Tutorial 目录中(参见准备工作), 把 Program.cs 写成宿主代码加上一行挂载:

ctx.Plugin(Plugin.From(c => Console.WriteLine("hello from my first plugin")));

Plugin.From(src/Cordis/Plugin.cs)把委托包成 IPlugin——这是 dsh 函数插件的对应物,也是本教程最常用的形态。

运行

dotnet run

预期输出:

hello from my first plugin

当没有任何内容继续运行时,进程会自行退出。具体过程:

  1. 宿主创建根 Context 并 StartAsync()。
  2. ctx.Plugin(...) 把插件挂载为一个子插件(fork + apply + 纳入激活图)。
  3. Cordis 在 fork 出的子 Context 上运行你的委托。

你的代码里没有框架启动代码:插件描述自己的贡献;到第 6 章, 组合关系会整体移到 YAML 文档里。

其他两种插件形态

IPlugin 接口只有 Inject 与 ApplyAsync 两个成员;Plugin 基类是同步插件的便捷形态:

// 1. 委托插件(刚才写的)
ctx.Plugin(Plugin.From(c => { }));

// 2. 类插件:继承 Plugin,等价于 dsh 的对象插件
public sealed class HelloPlugin : Plugin
{
    protected override void Apply(Context context)
        => Console.WriteLine("hello from class plugin");
}
ctx.Plugin(new HelloPlugin());

// 3. CordisService 子类(第 3 章介绍)

Context.Plugin 返回 PluginHandle——对应 dsh 的 fiber,是已挂载插件实例的运行时句柄: IsActive / IsDisposed 查询状态,Completion 等待终态,DisposeAsync() 显式卸载。 挂载动作本身注册为父 scope 的 effect:卸载父级会连带卸载插件。

ctx.Plugin() inject 齐 active IsActive == true apply 抛异常 failed Completion 失败 DisposeAsync()(或父 scope 回收) disposed effect 全部回收
还有第四个状态——inject 未满足时 handle 静默等待(PENDING),第 3 章与第 6 章展开。

尝试制造错误

让委托抛出异常:

var handle = ctx.Plugin(Plugin.From(_ => throw new InvalidOperationException("apply exploded")));
try
{
    await handle.Completion;
}
catch (PluginApplyException ex)
{
    Console.WriteLine($"plugin failed: {ex.InnerException?.Message}");
}

apply 失败会回收该插件的 scope 并使 PluginHandle.Completion 以 PluginApplyException 失败—— 不允许静默吞掉(src/Cordis/PluginHandle.cs)。这一点比 dsh 更显式:TS 版 fiber 失败体现在状态机里, C# 版可以直接 await 到异常。

下一章:生命周期与 effect:插件卸载时会发生什么。

在 GitHub 上编辑此页