跳到主要内容
版本:4.0

插件系统

定义

命名空间:
TouchSocket.Core
安装:
dotnet add package TouchSocket.Core

一、说明

插件系统是一组能实现多播订阅的、可中断的触发器,其主要功能就是实现类似事件、委托的通知消息机制。其设计核心来自于AspNetCore的中间件,它有着和中间件一样的使用体验,同时也有着更高的灵活性和自由度。

插件系统采用责任链模式,允许多个插件按顺序处理同一个事件,每个插件都可以决定是否继续传递给下一个插件,这种设计使得系统具有高度的可扩展性和灵活性。

二、产品特点

  • 简单易用 - 插件接口设计简洁,易于理解和使用
  • 高性能 - 支持IL调用和源生成器,性能接近直接调用
  • 易扩展 - 支持多种添加插件的方式,包括类型、实例和委托
  • 可中断 - 支持在任意插件中中断传递链
  • 可注入 - 完全支持依赖注入容器
  • 模块化 - 每个插件可以独立负责特定功能

三、产品应用场景

  • 所有可以使用事件、委托的场景
  • 需要中间件模式的场景
  • 需要责任链模式的场景
  • 需要模块化处理的业务逻辑
  • 需要动态扩展功能的系统

四、与事件、委托相比

插件系统相比传统的事件和委托机制有以下优势:

  1. 解耦订阅 - 订阅的时候可以不用知道被订阅方是谁,只需要知道要订阅什么通知即可
  2. 可中断传递 - 订阅可以随时中断。例如:当事件多播的时候,即使其中一个订阅方已经处理,触发也不会停止,这样会造成资源浪费。而插件可以在任意位置中断
  3. 支持回调 - 第一个订阅方想知道本次触发的最终结果是否已被处理时,委托则做不到,而插件则可以
  4. 可继承扩展 - 插件可以被继承,可以被扩展,更符合面向对象设计
  5. 依赖注入 - 插件可以被注入,方便管理生命周期
  6. 功能独立 - 插件可以独立负责相关功能,实现功能独立,可模块化功能
  7. 执行顺序可控 - 可以精确控制插件的执行顺序和传递链

五、创建插件

创建插件主要包括两个步骤:定义插件接口和实现插件接口。

5.1 新建插件接口及事件类

首先需要定义一个插件接口和对应的事件参数类。插件接口必须继承IPlugin,事件参数类必须继承PluginEventArgs

🔄 正在加载代码...

插件方法的定义必须遵循以下规则:

  1. 参数要求: 必须有两个参数
    • 第一个参数: 可以是任意类型,一般表示触发源(sender)
    • 第二个参数: 必须继承PluginEventArgs,用于传递事件数据
  2. 返回值要求: 必须是Task类型,支持异步操作
  3. 唯一性要求: 一个插件接口中只允许有一个插件方法
注意事项

确定插件唯一的是插件中的类型,所以要求一个插件中只允许有一个插件方法。如果需要处理多种不同的事件,应该定义多个插件接口。

5.2 实现插件接口

实现插件接口有两种方式:

方式一: 继承PluginBase基类(推荐)

新建一个类,先继承PluginBase基类,然后实现所需的插件接口。这种方式可以简化实现过程,PluginBase提供了一些便捷的基础功能。

方式二: 直接实现插件接口

如果类型已经有其他基类,则可以直接实现IPlugin接口和插件方法接口的全部内容。

🔄 正在加载代码...

在上面的示例中,我们创建了三个插件:

  • SayHelloPlugin: 处理"hello"消息,处理后中断传递
  • SayHiPlugin: 处理"hi"消息,处理后中断传递
  • LastSayPlugin: 兜底插件,处理所有未被前面插件处理的消息

每个插件都遵循以下模式:

  1. 进入插件 - 打印Enter日志
  2. 判断处理 - 检查是否满足处理条件
  3. 处理或传递 - 如果满足条件则处理并设置e.Handled = true;否则调用await e.InvokeNext()传递给下一个插件
  4. 离开插件 - 打印Leave日志(只有调用了InvokeNext才会执行到这里)
提示

实现的插件建议继承PluginBase,然后实现所需的插件接口,这样能简化实现过程。但是如果该类型已经拥有基类,则直接实现所需插件接口的全部内容即可。

六、订阅插件

在插件触发前,需要先订阅插件。插件管理器会按照添加的顺序依次调用各个插件,形成一个责任链。

6.1 创建插件管理器

首先需要创建一个PluginManager实例,并设置Enable = true启用插件管理器。

🔄 正在加载代码...

PluginManager构造函数需要传入一个IResolver容器对象,用于支持依赖注入功能。

6.2 添加订阅插件

TouchSocket提供了多种添加插件的方式,以适应不同的使用场景。

6.2.1 按类型添加

这是最常用的方式,通过泛型类型参数添加插件。插件实例会由容器自动创建。

🔄 正在加载代码...

这种方式的优点:

  • 代码简洁,不需要手动实例化
  • 支持依赖注入,插件的依赖会自动解析
  • 支持单例模式(通过PluginOption特性)

6.2.2 按实例添加

如果需要手动控制插件的创建过程,可以直接传入插件实例。

pluginManager.Add(new SayHelloPlugin());
pluginManager.Add(new SayHiPlugin());
pluginManager.Add(new LastSayPlugin());

这种方式的优点:

  • 可以在创建插件时传入构造参数
  • 可以对插件实例进行预配置
  • 完全控制插件的生命周期

6.2.3 按委托添加

委托添加是一种轻量级的方式,适合简单的处理逻辑,无需创建完整的插件类。

🔄 正在加载代码...

委托添加支持多种签名:

  1. 无参委托 - () => { } - 仅用于通知,不需要处理数据
  2. 单参委托 - (MyPluginEventArgs e) => { } - 只接收事件参数,需要手动调用e.InvokeNext()
  3. 双参委托(不指定类型) - (client, e) => { } - 接收发送者和事件参数,需要手动调用e.InvokeNext()
  4. 双参委托(指定类型) - (object client, MyPluginEventArgs e) => { } - 明确指定参数类型,需要手动调用e.InvokeNext()
提示

什么时候需要调用InvokeNext?

只需要记住一点:委托中是否接收了PluginEventArgs派生的事件参数。

  • 如果接收了事件参数,则需要主动调用await e.InvokeNext()
  • 如果没有接收事件参数(无参委托),则不需要调用,系统会自动继续执行

七、触发插件

当所有需要的插件都添加完成后,就可以通过RaiseAsync方法触发插件链的执行。

🔄 正在加载代码...

参数说明:

  • pluginType: 要触发的插件接口类型,例如typeof(ISayPlugin)
  • sender: 事件发送者,可以是任意对象,通常是触发事件的对象本身
  • eventArgs: 事件参数,必须是插件方法中定义的PluginEventArgs派生类型
重要提示

触发时传入的sender参数类型和PluginEventArgs参数类型必须和插件方法定义中的参数类型一致,否则会抛出异常。

执行流程:

  1. 插件管理器按照添加顺序依次执行每个插件
  2. 每个插件可以选择处理事件或调用e.InvokeNext()传递给下一个插件
  3. 如果某个插件设置了e.Handled = true或没有调用InvokeNext(),则停止传递
  4. 所有插件执行完成后返回处理结果

7.1 执行结果分析

按照上述代码逻辑,我们声明了一个名为ISayPlugin的插件接口,里面包含一个Say方法。然后分别创建了SayHelloPluginSayHiPluginLastSayPlugin三个类去实现ISayPlugin接口。将这三个类都添加至插件管理器中,然后触发Say方法,并传入不同的参数。

场景一: Words="test"

当输入"test"时,SayHelloPluginSayHiPlugin均不满足处理条件,所以会将数据转至下一个插件,直到LastSayPlugin插件。然后当LastSayPlugin处理结束以后,处理结果又按照LastSayPluginSayHiPluginSayHelloPlugin的顺序退出插件。

这体现了责任链模式的特点:即使前面的插件无法处理该数据,也能通过回调得知该数据最终有没有被处理。

场景二: Words="hello"

当输入"hello"时,SayHelloPlugin满足处理条件,设置e.Handled = true并终止插件的继续传递。后续的SayHiPluginLastSayPlugin不会被执行。

场景三: Words="hi"

当输入"hi"时,SayHelloPlugin不满足条件,调用InvokeNext()传递给下一个插件。SayHiPlugin满足条件,处理并终止传递。然后执行流程返回到SayHelloPluginInvokeNext()之后继续执行。

执行结果示例:

请输入hello,或者hi
test
SayHelloPlugin------Enter
SayHiPlugin------Enter
LastSayPlugin------Enter
您输入的test似乎不被任何插件处理
LastSayPlugin------Leave
SayHiPlugin------Leave
SayHelloPlugin------Leave

请输入hello,或者hi
hello
SayHelloPlugin------Enter
SayHelloPlugin------Say

请输入hello,或者hi
hi
SayHelloPlugin------Enter
SayHiPlugin------Enter
SayHiPlugin------Say
SayHelloPlugin------Leave
请输入hello,或者hi

从执行结果可以清晰看到插件的执行链和回调机制。

八、插件特性

8.1 中断传递

插件系统的一个重要特性是支持在任意位置中断传递链。有两种方式可以中断传递:

方式一: 设置Handled标志

e.Handled = true;
await e.InvokeNext(); // 即使调用InvokeNext,后续插件也不会执行

方式二: 不调用InvokeNext

if (满足处理条件)
{
// 处理逻辑
return; // 直接返回,不调用e.InvokeNext()
}

当某个插件中断传递后,该数据将不会再触发后续的插件,但是前面已经执行过的插件的"离开"逻辑仍会执行。

选择建议
  • 如果只是不想继续传递,使用return不调用InvokeNext()即可
  • 如果需要明确标记事件已被处理,建议设置e.Handled = true
  • 两种方式可以组合使用,以提高代码可读性

8.2 插件生命周期

插件有两个重要的生命周期方法(定义在IPlugin接口中):

Loaded(IPluginManager pluginManager)

当插件成功添加到IPluginManager时执行。可以在此方法中进行初始化操作,例如:

  • 注册额外的委托处理
  • 订阅其他插件事件
  • 初始化插件状态

Unloaded(IPluginManager pluginManager)

当插件通过IPluginManager.Remove()被移除时执行。可以在此方法中进行清理操作,例如:

  • 释放资源
  • 取消订阅
  • 保存状态

如果继承PluginBase,这两个方法已经有默认实现,可以通过override重写。

8.3 插件配置特性

可以使用PluginOption特性来配置插件的行为:

[PluginOption(Singleton = true)]
public class MySingletonPlugin : PluginBase, IMyPlugin
{
// 插件实现
}

Singleton属性

设置为true时,该插件在一个IPluginManager中只会有一个实例。即使多次调用Add<T>(),也只会使用第一次添加的实例。

FromIoc属性

设置为true时,插件实例将从IoC容器中解析,而不是每次都创建新实例。这对于需要依赖注入的插件非常有用。

九、提升插件性能

TouchSocket的插件系统在设计时充分考虑了性能问题,提供了多种优化方案。

9.1 插件性能测试

如下图所示,添加10个插件,并且调用10000次,即单个插件方法被调用10万次。

测试环境: 分别在net6.0、netcore3.1、net4.6.1上进行测试

测试项目说明:

  1. DirectRun(直接调用) - 直接调用方法的基准性能
  2. ActionRun(委托调用) - 使用委托调用的性能
  3. MethodInfoRun(反射调用) - 使用反射调用的性能
  4. ExpressionRun(表达式树调用) - 使用表达式树调用的性能
  5. PluginRun(插件调用) - 使用插件接口调用的性能
  6. PluginActionRun(插件委托调用) - 使用插件委托调用的性能

性能测试结论:

  • 插件内部使用的是IL调用,即使调用上有迭代,性能上也和直接调用的表达式树差不多
  • 插件采用递归方式迭代,表达式树测试则是直接for迭代,但性能差距不大
  • 使用插件委托调用时,性能可提升至接近直接调用,仅降低约20%
  • 插件委托调用没有任何动态代码生成,这意味着在Unity等AOT环境中也完全可行

9.2 注册委托

注册委托方式可以显著提升插件性能,推荐在性能敏感场景使用。

方式一: 直接添加委托

🔄 正在加载代码...

方式二: 在插件内注册方法为委托

不仅可以直接使用委托,还可以在插件里面注册方法为委托。

🔄 正在加载代码...

这种方式的优势:

  • 可以将插件逻辑封装在类中,保持代码组织性
  • 享受委托调用的高性能
  • 可以访问插件的实例成员和状态
注意事项

当使用注册方法为委托的方式时,不要再实现插件接口,否则会导致插件方法被调用两次:

  • 一次通过接口反射调用
  • 一次通过委托直接调用

这会造成重复处理和性能下降。

9.3 源生成插件

使用源生成器可以在编译时直接生成委托调用代码,获得最佳性能。

使用要求:

  • 编译器支持C# 源生成器(Visual Studio 2022及以上,或Rider)
  • 在插件接口上添加[DynamicMethod]特性
🔄 正在加载代码...

工作原理:

源生成器会在编译时分析标记了[DynamicMethod]的接口,自动生成高性能的调用代码。生成的代码会:

  • 避免反射调用的性能损耗
  • 直接使用委托调用,性能接近直接调用
  • 完全类型安全,编译时检查

使用建议:

一般情况下,你不需要关心源生成器的细节。当编译环境支持时,系统会自动使用源生成器优化插件调用。只需要:

  1. 在插件接口上添加[DynamicMethod]特性
  2. 正常实现和使用插件
  3. 编译器会自动处理优化
性能建议

根据使用场景选择合适的插件添加方式:

  • 开发阶段 - 使用按类型添加,代码简洁,易于调试
  • 性能敏感 - 使用委托添加或源生成插件,获得最佳性能
  • 简单逻辑 - 直接使用匿名委托,无需创建完整的插件类
  • 复杂逻辑 - 创建完整的插件类,便于代码组织和维护

十、最佳实践

10.1 插件设计原则

单一职责原则

每个插件应该只负责一个具体的功能,避免在一个插件中处理多个不相关的逻辑。

// ✅ 好的做法 - 单一职责
public class LoggingPlugin : PluginBase, IMessagePlugin
{
public async Task OnMessage(object sender, MessageEventArgs e)
{
// 只负责日志记录
Logger.Info($"收到消息: {e.Message}");
await e.InvokeNext();
}
}

// ❌ 不好的做法 - 职责混乱
public class AllInOnePlugin : PluginBase, IMessagePlugin
{
public async Task OnMessage(object sender, MessageEventArgs e)
{
// 日志、验证、转换...太多职责
Logger.Info($"收到消息: {e.Message}");
if (!Validate(e.Message)) return;
e.Message = Transform(e.Message);
await e.InvokeNext();
}
}

职责链顺序

合理安排插件的添加顺序,一般遵循:

  1. 验证类插件 - 最先执行,快速失败
  2. 转换类插件 - 处理数据格式
  3. 业务类插件 - 执行核心业务逻辑
  4. 日志类插件 - 记录处理结果
  5. 兜底类插件 - 处理未被处理的情况

状态管理

使用PluginEventArgs.State属性在插件间传递临时状态:

// 在前面的插件中设置状态
e.State = new { UserId = 123, Timestamp = DateTime.Now };

// 在后面的插件中读取状态
if (e.State is { UserId: int userId })
{
// 使用状态信息
}

10.2 错误处理

在插件中应该妥善处理异常,避免影响整个插件链:

public class SafePlugin : PluginBase, IMyPlugin
{
public async Task Process(object sender, MyEventArgs e)
{
try
{
// 插件逻辑
await DoSomethingAsync();
await e.InvokeNext();
}
catch (Exception ex)
{
// 记录异常
Logger.Error("插件执行失败", ex);

// 根据业务决定是否继续
// 选项1: 中断传递
e.Handled = true;

// 选项2: 继续传递给下一个插件
// await e.InvokeNext();
}
}
}

10.3 插件管理

移除插件

可以通过以下方式移除插件:

// 保存插件引用
var myPlugin = pluginManager.Add<MyPlugin>();

// 后续需要时移除
pluginManager.Remove(myPlugin);

// 或者移除委托
Action<object, MyEventArgs> handler = (s, e) => { };
pluginManager.Add(typeof(IMyPlugin), handler);
pluginManager.Remove(typeof(IMyPlugin), handler);

查询插件数量

// 获取指定类型插件的数量
var count = pluginManager.GetPluginCount(typeof(IMyPlugin));

// 获取从IOC容器创建的插件数量
var iocCount = pluginManager.GetFromIocCount(typeof(IMyPlugin));

禁用插件管理器

// 临时禁用所有插件
pluginManager.Enable = false;

// 重新启用
pluginManager.Enable = true;

10.4 与依赖注入集成

插件系统完全支持依赖注入:

// 在容器中注册服务
var container = new Container();
container.RegisterSingleton<IMyService, MyService>();

// 创建插件管理器时传入容器
var pluginManager = new PluginManager(container);

// 插件可以注入依赖
public class MyPlugin : PluginBase, IMyPlugin
{
private readonly IMyService _service;

// 通过构造函数注入
public MyPlugin(IMyService service)
{
_service = service;
}

public async Task Process(object sender, MyEventArgs e)
{
await _service.DoSomethingAsync();
await e.InvokeNext();
}
}

10.5 单元测试

插件的可测试性很强:

[Test]
public async Task TestMyPlugin()
{
// Arrange
var container = new Container();
var pluginManager = new PluginManager(container);
pluginManager.Add<MyPlugin>();

var eventArgs = new MyEventArgs { Data = "test" };

// Act
var handled = await pluginManager.RaiseAsync(
typeof(IMyPlugin),
this,
eventArgs
);

// Assert
Assert.IsTrue(handled);
Assert.AreEqual("processed", eventArgs.Result);
}

十一、示例Demo