在C#中建立模块化程序的最佳方法是什么?

22

我和我的朋友正在编写一款 IRC C# 机器人,我们希望能够包含一个模块系统,以便用户可以编写自定义模块来扩展功能。

该机器人使用正则表达式将服务器的所有原始数据分解,并触发相应的事件来处理数据。例如,典型的事件处理程序可能如下所示:

OnChannelMessage(object sender, ChannelMessageEventArgs e)
{
}
ChannelMessageEventArgs中会包含频道名称、发送者的昵称、消息等信息。我希望有一个插件系统,让人们可以构建模块并在机器人加载时或运行期间随意加载/卸载它们。理想情况下,我希望能够即时编译.cs文件,并在plugin.cs文件中包含以下几个内容:
  1. 要捕获的事件,例如OnChannelMessage以及在OnChannelEventArgs中的Channeldata
  2. 当给出这些信息时要做什么
  3. 帮助文本(我可以从主机器人内部调用,比如一个字符串help =“ 这是此插件的帮助”,可以在任何时候返回而不实际调用插件)
  4. 插件名称等
感谢任何关于新手如何开始编程的想法。
5个回答

37

我以前在项目中使用过这样的东西,但已经有一段时间了。可能有一些框架可以为您执行这种类型的操作。

要编写自己的插件架构,基本上要定义一个接口,让所有模块都实现它,并将其放在程序和模块都共享的程序集中:

public interface IModule
{
     //functions/properties/events of a module
} 

然后您的实现人员将根据此程序集编写他们的模块,最好使用默认构造函数。
public class SomeModule : IModule {} ///stuff 

在您的程序中(假设您的模块将编译为自己的程序集),您需要加载包含模块的程序集引用,找到实现模块接口的类型,并对它们进行实例化。
var moduleAssembly = System.Reflection.Assembly.LoadFrom("assembly file");
var moduleTypes = moduleAssembly.GetTypes().Where(t => 
   t.GetInterfaces().Contains(typeof(IModule)));

var modules = moduleTypes.Select( type =>
            {   
               return  (IModule) Activator.CreateInstance(type);
            });

如果你想即时编译代码:你需要创建一个编译器对象,告诉它需要引用哪些程序集(包括System和包含IModule的那个程序集,以及任何其他必要的引用),然后告诉它将源文件编译成一个程序集。从中获取导出类型,过滤并保留实现IModule接口的类型,并对它们进行实例化。
//I made up an IModule in namespace IMod, with string property S
string dynamicCS = @"using System; namespace DYN 
             { public class Mod : IMod.IModule { public string S 
                 { get { return \"Im a module\"; } } } }";

var compiler = new Microsoft.CSharp.CSharpCodeProvider().CreateCompiler();
var options = new System.CodeDom.Compiler.CompilerParameters(
       new string[]{"System.dll", "IMod.dll"});

var dynamicAssembly= compiler.CompileAssemblyFromSource(options, dynamicCS);
//you need to check it for errors here

var dynamicModuleTypes = dynamicAssembly.CompiledAssembly.GetExportedTypes()
    .Where(t => t.GetInterfaces().Contains(typeof(IMod.IModule)));
var dynamicModules = dynModType.Select(t => (IMod.IModule)Activator.CreateInstance(t));

查找有关插件架构和加载动态程序集的教程,以更好地了解如何进行此类操作。这只是一个开始。一旦掌握了基础知识,您就可以开始做一些非常酷的事情。

至于处理元数据(模块X命名为YYY,应处理事件A、B和C): 尝试将其纳入您的类型系统中。您可以为不同的功能/事件制作不同的接口,或者将所有功能放在一个接口上,并在模块类上使用属性(您将在共享程序集中声明这些属性),使用属性来声明模块应订阅哪些事件。基本上,您希望使人们为您的系统编写模块尽可能简单。

 enum ModuleTypes { ChannelMessage, AnotherEvent, .... }

 [Shared.Handles(ModuleTypes.ChannelMessage)]
 [Shared.Handles(ModuleTypes.AnotherEvent)]
 class SomeModule : IModule { ... }

或者

 //this is a finer-grained way of doing it
 class ChannelMessageLogger : IChannelMessage {}
 class PrivateMessageAutoReply : IPrivateMessage {} 

玩得开心!


1
我强烈建议使用现有的插件框架,例如MEF或System.AddIns;它们可以为您节省大量的麻烦。 - Judah Gabriel Himango
1
我也是,但是原帖要求非框架解决方案。此外,掌握基础知识也很重要。 - dan
3
如果我可以再点几次赞,我就会这样做。我从你3年前提供的代码中学到了很多。 - caesay
如果你想要即时编译代码,那么为什么会这样呢?我想不出任何理由。我的意思是,我应该提前准备好插件dlls。为什么我要在程序中创建这些dlls,而不是在Visual Studio、MSBuild或构建服务器中创建呢? - yakya
@sotn 为什么你会想要任何非预编译/解释的脚本语言呢?这个(将近十年前)的最初目标是允许添加、删除和编辑小型脚本(用 C# 编写,以简化操作),而无需将底层系统下线造成停机时间。虽然这些脚本可以预先编译,但它们的性质是不断调整的,预编译只会增加开销。我认为 SOF 上的人们的工作不是问别人为什么需要某些东西。 - caesay
显示剩余3条评论

18

Managed Extensibility Framework (MEF)提供了您所需的插件架构。您可以提供一个公共接口,供用户进行扩展。然后,MEF可以扫描目录中的DLL并动态加载任何可导出项。

例如,您的插件作者可以创建类似于:

[Export(typeof(IModule))]
public class MyModule : IModule
{
   void HandleMessage(ChannelEventArgs e) {}
}

那么你的代码可能会像这样:

void OnChannelMessage(ChannelEventArgs e)
{
  foreach(IModule module in Modules)
  {
     module.HandleMessage(e);
  }
}

[Import]
public IEnumerable<IModule> Modules { get; set; }

有没有一种方法可以在不使用任何外部框架的情况下完成这样的事情? - caesay
1
这是开源的,所以你可以查看,但是使用 FileSystemWatcherAssembly.Load 和一些反射技术,你应该能够相对容易地建模出类似的东西。 - bendewey
5
你可以自己编写框架代码...不过为什么要重复造轮子呢? - Paolo
导入方面比导出方面要困难得多。您可能希望避免使用setter注入部分,而只是使用一个辅助函数来获取导入内容。 - bendewey
1
@Paolo同意,特别是因为这将与.NET4一起发布。 - bendewey

4

1
另外值得注意的是,它将成为.NET 4.0的一部分。 - Wim Coenen

3

2

MEF - 托管可扩展性框架是当前的热门话题,我认为这将是最合适的选择。

如果这行不通,你还可以查找Prism,他们有另一种实现模块的方法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接