C# 插件架构与插件之间的接口共享

Posted

技术标签:

【中文标题】C# 插件架构与插件之间的接口共享【英文标题】:C# Plugin Architecture with interfaces share between plugins 【发布时间】:2010-10-24 04:42:33 【问题描述】:

我将我的问题分为短版和长版,供手头时间不多的人使用。

短版:

我需要一些架构来构建具有提供者和消费者插件的系统。 提供者应该实现接口 IProvider,消费者应该实现 IConsumer。 执行应用程序应该只知道 IProvider 和 IConsumer。 消费者实现可以询问正在执行的程序集(通过 ServiceProcessor)哪些提供者实现了 InterfaceX 并获取一个 List。 这些 IProvider 对象应该被强制转换为 InterfaceX(在消费者中),以便能够将消费者挂钩到 InterfaceX 定义的某些事件上。这将失败,因为执行程序集不知何故不知道此 InterfaceX 类型(转换失败)。解决方案是将 InterfaceX 包含到插件和执行程序集都引用的某个程序集中,但这应该意味着对每个新的提供者/消费者对都进行重新编译,这是非常不可取的。

有什么建议吗?

长版:

我正在开发某种通用服务,它将使用插件来实现更高级别的可重用性。该服务由某种使用提供者和消费者的观察者模式实现组成。提供者和消费者都应该是主应用程序的插件。让我首先通过列出我的解决方案中的项目来解释服务的工作原理。

项目 A:用于托管所有插件和基本功能的 Windows 服务项目。 TestGUI Windows 窗体项目用于更轻松的调试。来自项目 B 的 ServiceProcessor 类的一个实例正在执行与插件相关的工作。该项目的子文件夹“Consumers”和“Providers”包含子文件夹,其中每个子文件夹分别包含一个消费者或提供者插件。

项目 B:一个类库,包含 ServiceProcessor 类(负责插件之间的所有插件加载和调度等)、IConsumer 和 IProvider。

项目C:一个类库,链接到项目B,由TestConsumer(实现IConsumer)和TestProvider(实现IProvider)组成。 TestProvider 实现了一个附加接口(ITest,它本身从 IProvider 派生)。

这里的目标是消费者插件可以询问服务处理器它有哪些提供者(至少实现 IProvider)。返回的 IProvider 对象应在 IConsumer 实现中强制转换为它实现的其他接口 (ITest),以便使用者可以将事件处理程序挂钩到 ITest 事件。

当项目 A 启动时,会加载包含使用者和提供者插件的子文件夹。以下是我目前遇到并尝试解决的一些问题。

ITest 曾经驻留在项目 C 中的接口,因为这只适用于 TestProvider 和 TestConsumer 知道的方法和事件。总体思路是让项目 A 保持简单,并且不知道插件之间的作用。

使用项目 C 中的 ITest 和 TestConsumer 的 Initialize 方法中的代码,该方法将 IProvider 转换为 ITest(当实现 ITest 的对象被称为 IConsumer 对象时,这不会在单个类库本身中失败)无效的转换会发生错误。可以通过将 ITest 接口放入项目 A 引用的项目 B 中来解决此错误。这是非常不受欢迎的,因为我们需要在构建新接口时重新编译项目 A。

我尝试将 ITest 放在仅由项目 C 引用的单个类库中,因为只有提供者和消费者需要了解此接口,但没有成功:加载插件时,CLR 指出引用的项目不能被发现。这可以通过挂钩当前 AppDomain 的 AssemblyResolve 事件来解决,但不知何故这似乎也不需要。 ITest 又回到了 Project B。

我尝试将项目 C 拆分为消费者和提供者的单独项目,并加载本身运行良好的程序集:两个程序集都驻留在 Assemblies 集合或当前 AppDomain 中: 发现程序集:Datamex.Projects.Polaris.Testing.Providers,Version=1.0.0.0,Culture=neutral,PublicKeyToken=2813de212e2efcd3 发现程序集:Datamex.Projects.Polaris.Testing.Consumers,Version=1.0.0.0,Culture=neutral,PublicKeyToken=ea5901de8cdcb258

由于消费者使用提供者,因此消费者对提供者进行了引用。现在再次触发 AssemblyResolve 事件,说明它需要以下文件: AssemblyName=Datamex.Projects.Polaris.Testing.Providers,版本=1.0.0.0,文化=中性,PublicKeyToken=2813de212e2efcd3

我的问题: 为什么是这样?这个文件已经加载对了吗? 为什么从 IProvider 转换到我知道它实现的某个接口是不可能的?这可能是因为执行程序本身不知道这个接口,但不能动态加载?

我的最终目标: 消费者插件询问 ServiceProcessor 它有哪些提供者实现了接口 x。提供者可以被强制转换到这个接口 x,而无需执行程序集知道接口 x。

有人可以帮忙吗?

提前致谢, 埃里克

【问题讨论】:

冒着听起来油嘴滑舌的风险,你需要尽量缩短这个问题。 我在长版的基础上添加了一个短版 :-) 很抱歉这篇长文,但很难描述我的问题。 【参考方案1】:

如果您的问题是,两个不相关的程序集如何共享同一个接口,答案是“你不能”解决方案是将接口包含在所有程序集中,也许在插件构建器可以引用的 dll 中,并在您的加载程序集中。

【讨论】:

问题实际上是:2 个相关的程序集(提供者和消费者)如何共享一个接口(在必要时通过单独的类库),该接口本身是从执行程序集知道的接口派生的。执行程序集只知道有提供者和消费者,但不知道他们之间有一些更详细的合同。【参考方案2】:

我已经做了类似于您正在尝试做的事情,只要我将程序集放在加载程序自动查看的位置,我就没有遇到任何问题。

您是否尝试过将所有程序集放在 exe 所在的子目录中?我现在不记得细节了,但是有一个步骤列表记录了加载程序在哪里以及以什么顺序查找程序集/类型。

【讨论】:

嗯,这可能是什么。插件不在执行程序集的子文件夹中,而是在可以配置的文件夹中。正在执行的程序集使用 Assembly.LoadFile() 加载提供者和消费者插件(我验证它们在当前 AppDomain 中)。尽管无法在消费者中进行从 IProvider 到 IMoreDetailedProvider 的强制转换,除非此 IMoreDetailedProvider 驻留在执行程序集引用的单独类库中。 这就是为什么我认为这可能是加载程序问题。另外,请仔细检查您保存的类型规范。【参考方案3】:

我只是尽我所能重新创建您的解决方案,我没有这样的问题。 (警告,后面有很多代码示例......)

第一个项目是应用程序,它包含一个类:

public class PluginLoader : ILoader

    private List<Type> _providers = new List<Type>();

    public PluginLoader()
    
        LoadProviders();
        LoadConsumers();
    

    public IProvider RequestProvider(Type providerType)
    
        foreach(Type t in _providers)
        
            if (t.GetInterfaces().Contains(providerType))
            
                return (IProvider)Activator.CreateInstance(t);
            
        
        return null;
    

    private void LoadProviders()
    
        DirectoryInfo di = new DirectoryInfo(PluginSearchPath);
        FileInfo[] assemblies = di.GetFiles("*.dll");
        foreach (FileInfo assembly in assemblies)
        
            Assembly a = Assembly.LoadFrom(assembly.FullName);
            foreach (Type type in a.GetTypes())
            
                if (type.GetInterfaces().Contains(typeof(IProvider)))
                
                    _providers.Add(type);
                
            
        

    

    private void LoadConsumers()
    
        DirectoryInfo di = new DirectoryInfo(PluginSearchPath);
        FileInfo[] assemblies = di.GetFiles("*.dll");
        foreach (FileInfo assembly in assemblies)
        
            Assembly a = Assembly.LoadFrom(assembly.FullName);
            foreach (Type type in a.GetTypes())
            
                if (type.GetInterfaces().Contains(typeof(IConsumer)))
                
                    IConsumer consumer = (IConsumer)Activator.CreateInstance(type);
                    consumer.Initialize(this);
                
            
        
    

显然,这可以极大地整理。

下一个项目是共享库,包含以下三个接口:

public interface ILoader

    IProvider RequestProvider(Type providerType);


public interface IConsumer

    void Initialize(ILoader loader);


public interface IProvider


终于有了这些类的插件项目:

public interface ITest : IProvider
        


public class TestConsumer : IConsumer

    public void Initialize(ILoader loader)
    
        ITest tester = (ITest)loader.RequestProvider(typeof (ITest));
    


public class TestProvider : ITest
        

应用程序和插件项目都引用共享项目,并且插件 dll 被复制到应用程序的搜索目录 - 但它们不相互引用。

构造 PluginLoader 时,它会找到所有 IProvider,然后创建所有 IConsumer 并在它们上调用 Initialize。在初始化过程中,消费者可以从加载器请求提供者,在此代码的情况下,构造并返回一个 TestProvider。所有这些都适用于我,无需对程序集的加载进行花哨的控制。

【讨论】:

非常感谢。我会深入研究并告诉你! 我试过你的代码,它可以工作。再次感谢。但是,它与我已经制作的(不起作用)并没有太大区别。我会发布我所做的解决方案,也许你可以发现问题,因为我仍然没有线索:-) 我终于弄清楚我做错了什么。只有一种方法可以在工作解决方案和几个小时或调试之间产生差异:您的程序集加载代码:Assembly a = Assembly.LoadFrom(assembly.FullName);我错误的加载代码是:Assembly assembly = Assembly.LoadFile(filename);当我将其更改为 LoadFrom 时,一切正常!再次感谢您的宝贵时间!【参考方案4】:

您可能会发现我的文章有助于查看插件框架的工作示例,以及如何通过创建包含接口的通用程序集来解决这些问题:

C# 基础教程中的插件:

http://www.codeproject.com/KB/cs/pluginsincsharp.aspx

后续文章,带有启用泛型的插件管理器库:

http://www.codeproject.com/KB/cs/ExtensionManagerLibrary.aspx

【讨论】:

【参考方案5】:

它仍在开发中,但听起来像是 MEF 的完美用例(将包含在 .Net 4 中)并在 VS2010 内部使用。

MEF 提供了一个简单的解决方案 运行时可扩展性问题。直到 现在,任何想要的应用程序 支持所需的插件模型 创建自己的基础设施 刮。这些插件通常是 特定于应用程序,不能 跨多个重用 实现。

已在http://www.codeplex.com/MEF 上提供预览

Glen Block 的 blog 也很有用。

【讨论】:

我打算回答他应该只阅读 MEF,所以我只是投票赞成你的答案。 MEF 解决了处理所有这些问题的后勤问题,他们的文档和文章涵盖了如何定义和共享接口等概念的理论。我强烈建议查看 MEF 而不是自己构建。

以上是关于C# 插件架构与插件之间的接口共享的主要内容,如果未能解决你的问题,请参考以下文章

MEF插件架构上的“即发即弃”方法

如何在 Flutter Dart 中实现插件架构

什么是 QT 插件?

关于如何实现具有类似插件架构的 c# 主机应用程序的问题

在C#程序中实现插件架构

分享在winform下实现模块化插件编程