保持活动对象的插件系统

时间:2015-12-04 20:46:10

标签: c# dll plugins .net-assembly appdomain

我有一个应用程序需要在运行时加载和卸载.dll文件形式的一些插件。这些dll文件包含一个或多个派生自我的抽象类Module的类,如下所示:

public abstract class Module : MarshalByRefObject
{
    //various fields here, not reported as they are not useful to understand

   public abstract void Print();

}

根据我的理解:C#中卸载程序集及其类的唯一方法是将程序集放在专用的Appdomain中,然后在不再需要它时卸载它,以及在AppDomain之间进行通信的唯一方法是从MarshalByRefObject或MarshalByValueObject派生。然后,当我将模块的引用传递给另一个AppDomain时,我并没有真正获得该对象,而是一个透明的代理。任何人都可以确认我上面写的内容吗?

现在好了真正的问题:让我们在我的文件PersonPlugin.dll中说我有一个" Person"扩展Module的类。此类包含字段名称,姓氏和电话号码。

定期调用的方法Print(定期调用打印没有意义,但它就是一个例子)打印这3个字段。

后来我使用Person类的新版本构建了一个新的dll,它有一个名为address的新字段,Print方法现在也打印了该地址。 注意:插件是由第三方制作的,我不知道新版本是否与旧版本相似或完全不同,但在我的情况下,可以安全地假设一个类人员不会版本之间差异很大。

然后我用这个新文件替换旧的dll文件(dll在专用的appdomain中进行了阴影复制,因此该文件是可写/可删除的)

FileSystemWatcher注意到更改并且:

  1. 为新dll

  2. 创建一个新的appdomain
  3. 保存旧实例化Person对象的状态

  4. 将Person对象从旧的Person类分配给新的

  5. 卸载包含旧Person类

  6. 的appdomain
  7. 应用程序开始打印包含地址的新字符串(至少在开头时为空)

  8. 第2点和第3点是我的问题的核心:如何在卸载它们之后保持这些对象(或者至少是它们的字段),然后实例化一个具有相同名称和相似属性的新类,但是实际上是另一类?

    这是我到目前为止所做的:

    1. 使用反射我可以在字典中复制每个Person对象的每个字段,删除旧对象,然后实例化新对象并尽可能复制字段(如果没有旧字段,则可以使用它)跳过一个如果有一个新的它获得其默认值)

    2. 使用MessagePack之类的东西自动序列化旧的Person对象,然后将它们反序列化为新的Person对象

    3. 然而,我没有做任何测试,看看这2个解决方案是否可行,主要是因为我想在开始编写代码之前知道如果这两个解决方案可能在现实中工作或者只是在我脑海中因为也许你们中的一些人有更好的工作解决方案,甚至更好的框架/库已经做到了。

      更新

      好的,我意识到在没有背景化的情况下提出这个问题可能会导致一些误解,所以: 我问的问题是关于我的论文,它是一个模块化的最小游戏引擎(不是服务器)。我们的想法是使引擎模块化,并且为了简化测试各种功能,可以在运行时更改其模块,立即应用更改,而无需重新启动或失去“演员”的状态。在游戏里。 一个DLL包含1到多个模块。每个dll都加载到1个AppDomain中,所以如果我卸载该appdomain,我会卸载其中的每个模块。同一appdomain中的模块可以直接引用每个人,因为卸载时它们会一起卸载。不同程序集中的模块(以及不同的AppDomain)使用消息总线进行通信,并且永远不会直接引用。如果卸载消耗该消息的模块,系统将停止将该消息转发到该模块。

      重要的是模块所代表的内容取决于引擎的用户:模块可以代表单个游戏对象,如汽车或整个物理模块。我不想进一步挖掘细节,因为它们现在没用,但我想要实现的目标可能就是这样:

      我有一个dll包含一个名为Car的模块,它始终向前移动。我改变了一个新的包含模块Car的dll,现在它总是向后移动。 结果是,一旦我更换了dll,汽车将立即反转它的方向。

      当然这个例子很愚蠢,有一些方法可以用更简单的方式实现这一点。但是,这也适用于我在代码中发现错误,更正错误,提交错误,错误消失甚至更复杂情况的情况。

      这就是为什么我需要保持活着物体(我的意思是,让他们的状态保持活力)以及整个系统。

      关于你的观点2:对于为什么我不允许共存两个相同类型但不同版本的模块没有硬性限制,只要它们位于不同的命名空间或不同的dll中,因为在我的系统中每个模块都是使用不同的ID识别和引用

2 个答案:

答案 0 :(得分:2)

我认为你真的想要玩大黄蜂的巢。

您没有提及一些设计注意事项

1)为什么你需要一直“在线”。为什么你不能离线说每周一次重启1小时。无论您实施什么解决方案(反射/序列化)都会给您带来性能成本,更不用说维护上的巨额开销。

2)你是否有可能想要同一个插件的2个版本。在java世界中,有一个名为OSGI的框架。你所做的是你依赖某个模块,但在某些情况下,同一个模块的2个版本可以同时在线。特别是当一个人类添加一个地址字段然后所有人需要升级一些如何处理这个。此外,所有依赖于人员模块的模块都需要同时升级。

我并不是说没有理由不拥有你想要的东西。但我认为你应该重新考虑你的方法,看看你是否以正确的方式满足要求。

那说我认为你应该看一下微服务架构。然后每个模块将是它自己的webapi。每个模块都负责保持自己的存活(你可以在所有模块周围放置一些小包装器以避免代码重复)。然后你可以使用webapi调用一个模块,你的标准newtonsoft json序列化将处理其余的(和/或消息总线,类似的想法,稍微不同的实现)。 优点是,如果你调用/ api / print,那么模块将打印该人。如果你有一个新的模块,由于某种原因无法实现打印但实现/ api / writeline,那么模块将负责实现逻辑,也可以将旧的/ api / print转换为新的/ api / writeline。

消息总线的实施可能会更好地满足您的需求。您只需对要处理的命令进行排队,如果升级后模块重新联机,它将开始处理来自消息总线的所有消息。

答案 1 :(得分:1)

首先,让我评论你的第一点。

  

从我的理解:C#卸载程序集的唯一方法   它的类是将程序集放在专用的Appdomain中然后   当它不再需要时卸载它并且是唯一的通信方式   AppDomains之间的关系来源于MarshalByRefObject或   MarshalByValueObject。然后,当我将模块的引用传递给   另一个AppDomain我不是真正得到的对象,但透明   代理。任何人都可以确认我上面写的内容吗?

<强>确认。

这完全准确。加载后,无法从AppDomain卸载.NET程序集。必须卸载整个AppDomain

对于模块化系统,您必须在主机应用程序和插件之间的单独AppDomain中加载插件并填充数据。

现在,解决有关即时加载模块并保持其状态的问题。您绝对可以这样做,但您需要在模块和主机应用程序之间建立合同。

没有必要将您的模块标记为MarshalByRefObject,因为这会对模块实现带来额外的限制,并带来额外的开销。相反,我会通过使用接口来表示合同,使我的模块合同尽可能轻。

public interface IModule
{
    Person GetPerson();
    void Print();
}

需要在宿主应用程序和模块之间传递的任何数据都需要从MarshalByRefObject继承,因此如果您打算来回传递它,您的Person类应该看起来像这样。

public class Person : MarshalByRefObject
{
    ...
}

现在,关于保持Person的状态,即使已经添加了新属性。

由于您的主机应用程序不需要了解这些新属性,因此您应该将这些类型的对象的持久性移动到ModuleManager内,该AppDomain位于插件的IModule内,在public interface IModule { Person GetPerson(); void Print(); IDictionary<string, object> GetState(); void SetState(IDictionary<string, object> state); } public class ModuleManager : MarshalByRefObject { public IModule Module { get; set; } public void Initialize(string path) { // Load the module // Setup file watcher } public void SaveState(string file) { var state = this.Module.GetState(); // Write this state to a file } public void LoadState(string file) { var state = this.ReadState(file); this.Module.SetState(state); } private IDictionary<string, object> ReadState(string file) { ... } } 上公开一些知道如何进行实际持久性的方法。

ModuleManager

Person应该负责保存模块的状态,因此每个模块开发人员都不必实现此功能。它所要做的就是在键/值对字典中读取和写入状态。

您会发现此方法易于管理,而不是尝试在主机应用程序中执行所有持久性。

这里的想法是主机应用程序和插件之间尽可能少的数据。每个插件都知道csv中可用的字段,因此让他们负责指出需要保留的内容。