保持活动对象的插件系统

Plugin system that keeps alive objects

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

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

   public abstract void Print();

}

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

现在是真正的问题:假设在我的文件 PersonPlugin.dll 中,我有一个 "Person" class 扩展模块。此 class 包含字段名称、姓氏和 phone 号码。

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

稍后我用 class Person 的新版本构建了一个新的 dll,它有一个名为地址的新字段,Print 方法现在也打印地址。 注意:插件是由第 3 方制作的,我不知道新版本是与旧版本相似还是完全不同,但就我而言,可以安全地假设 class Person 之间不会有太大差异版本。

然后我用这个新文件替换旧的 dll 文件(dll 在专用的应用程序域中被卷影复制,因此文件是可写/可删除的)

FileSystemWatcher 注意到更改并且:

  1. 为新的 dll 创建一个新的应用域

  2. 保存旧实例化 Person 对象的状态

  3. 将 Person 对象从旧 Person class 分配给新 Person

  4. 卸载包含旧 Person 的应用域 class

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

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

到目前为止我是这样的:

  1. 使用反射我可以在字典中复制每个 Person 对象的每个字段,删除旧对象然后实例化新对象并在可能的情况下复制回字段(如果旧字段不存在它是跳过一个如果有新的它会得到它的默认值)

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

但是我没有做任何测试来查看这两种解决方案是否可行,主要是因为我想在开始编写代码之前知道这两种解决方案是否可以在现实中或仅在我的脑海中起作用,并且因为也许你们中的一些人有更完善/有效的解决方案,或者甚至更好的框架/库已经做到了。

更新:

好的,我意识到在不考虑上下文的​​情况下问这个问题可能会导致一些误解,所以: 我要问的问题是关于我的论文,这是一个模块化的最小游戏引擎(不是服务器)。这个想法是使引擎模块化,并且为了简化测试各种功能,可以在运行时更改其模块,立即应用更改,而无需重新启动或丢失游戏中 "actors" 的状态。 一个 dll 包含 1 到多个模块。每个 dll 都加载到 1 个 AppDomain 中,因此如果我卸载该应用程序域,我将卸载其中的每个模块。同一应用程序域内的模块可以直接引用彼此,因为卸载时它们会一起卸载。不同程序集中的模块(不同的 AppDomains 也是如此)使用消息总线进行通信,从不直接引用。如果卸载使用该消息的模块,系统将停止将该消息转发给该模块。

重要的是,模块代表什么取决于引擎的用户:模块可以代表单个游戏对象(如汽车)或整个物理模块。我不会深入研究更多细节,因为它们现在没用,但我想要实现的可能是这样的:

我有一个 dll,其中包含一个名为 Car 的模块,它总是向前移动。我用一个包含现在总是向后移动的模块 Car 的新 dll 更改了 dll。 结果是我一替换dll,小车就立马反转

当然这个例子很愚蠢,有一些方法可以更简单地实现这一点。然而,这也适用于我在我的代码中发现错误的情况,我更正它,提交它,错误就消失了,或者更复杂的情况。

这就是为什么我需要让对象保持活动状态(我的意思是,让它们的状态保持活动状态)以及整个系统。

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

我觉得你很想玩马蜂窝。

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

1) 为什么你需要一直 'online'。为什么你不能离线,比如每周重启一次 1 小时。无论您实施什么解决方案 (reflection/serializaiton) 都会给您带来性能成本,更不用说维护方面的巨大开销了。

2) 您是否可能想要同一个插件的两个版本。在 java 世界中,有一个名为 OSGI 的框架。你在那里做的是依赖某个模块,但在某些情况下,同一模块的两个版本可能同时在线。特别是当一个人 class 添加了一个地址字段时,所有的人都需要升级一些如何处理这个问题。另外所有依赖person模块的模块也需要同时升级。

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

也就是说,我认为您应该看看微服务架构。然后每个模块将例如是它自己的 webapi。每个模块负责保持自身活动(您可以对所有模块进行一些小包装以避免此处的代码重复)。然后您可以使用 webapi 调用一个模块,您的标准 newtonsoft json 序列化将处理其余部分(and/or 消息总线,那里的想法相似,实现略有不同)。 优点是如果你调用 /api/print 那么模块会打印出这个人。如果您有一个新模块由于某种原因无法实现 print 但实现了 /api/writeline 那么该模块将负责实现逻辑,也将调用从旧的 /api/print 转换为新的 [=28] =].

消息总线实现可能更适合您的需求。您只需将要处理的命令排队,如果升级后您的模块重新联机,它就会开始处理来自消息总线的所有消息。

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

From what I understood: the only way in C# to unload an assembly and its classes is to put the assembly in a dedicated Appdomain and then unload it when it's not needed anymore and the only way to communicate between AppDomains is to derive from MarshalByRefObject or MarshalByValueObject. Then When I pass a reference of the Module to another AppDomain I don't really get the object but a transparent proxy. Can anyone confirm what i wrote above please?

已确认。

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

在模块化系统的情况下,您必须在单独的 AppDomain 中加载插件,并在主机应用程序和插件之间编组数据。

现在,解决您关于动态加载模块和保持其状态的问题。您完全可以这样做,但是您需要在您的模块和您的主机应用程序之间建立一个契约。

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

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

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

public class Person : MarshalByRefObject
{
    ...
}

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

由于您的主机应用程序不需要了解这些新属性,因此您应该将这些类型的对象的持久性移动到一个 ModuleManager 中,该 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 应该负责保存模块的状态,这样每个模块开发人员就不必实现此功能。它所要做的就是从 key/value 对的字典中读取和写入它的状态。

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

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