具有插件之间共享接口(interface)的 C# 插件架构

标签 c# plugins interface observer-pattern

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

精简版:

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

有什么建议?

长版:

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

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

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

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

这里的目标是消费者插件可以询问 ServiceProcessor 它有哪些提供者(至少实现 IProvider)。返回的 IProvider 对象应该被转换到它在 IConsumer 实现中实现的另一个接口(interface) (ITest),以便消费者可以将事件处理程序挂接到 ITest 事件。

项目 A 启动时,将加载包含使用者和提供者插件的子文件夹。以下是我迄今为止遇到并试图解决的一些问题。

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

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

我试图将 ITest 放在仅由项目 C 引用的单个类库中,因为只有提供者和使用者需要知道这个接口(interface),但没有成功:加载插件时,CLR 声明找不到引用的项目。这可以通过 Hook 当前 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,版本=1.0.0.0,Culture=neutral,PublicKeyToken=ea5901de8cdcb258

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

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

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

有人可以帮忙吗?

提前致谢,
埃里克

最佳答案

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

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

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);
                }
            }
        }
    }

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

下一个项目是共享库,包含以下三个接口(interface):
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,然后创建所有 IConsumers 并调用它们的 Initialize。在初始化内部,消费者可以从加载器请求提供者,在此代码的情况下,一个 TestProvider 被构造并返回。所有这些都对我有用,对程序集的加载没有花哨的控制。

关于具有插件之间共享接口(interface)的 C# 插件架构,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/829597/

相关文章:

c# - WPF双击编辑标签

c# - 执行并行分解时发生溢出异常

C# DLL 配置文件不存在

android - Godot 扩展 c++ 模块与 android 模块。有什么不同?

java - 是什么让这些方法调用返回这些值?

c# - 从 UpdatePanel/javascript 调用 C# getter - 永远不会获取更新的数据

java - 使用 UI 创建 Android Studio 插件

jquery - JQuery 中的 new 运算符意味着

testing - 我如何使用 JustMock 来测试接口(interface)事件委托(delegate)

javascript - TypeScript 中的条件接口(interface)