具有插件之间共享接口的 C# 插件架构 [英] C# Plugin Architecture with interfaces share between plugins

查看:17
本文介绍了具有插件之间共享接口的 C# 插件架构的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I divided my problem into a short and a long version for the people with little time at hand.

简短版本:

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

I need some architecture for a system with provider and consumer plugins. Providers should implement intereface IProvider and consumers should implement IConsumer. The executing application should only be aware of IProvider and IConsumer. A consumer implementation can ask the executing assembly (by means of a ServiceProcessor) which providers implement InterfaceX and gets a List back. These IProvider objects should be casted to InterfaceX (in the consumer) to be able to hook the consumer onto some events InterfaceX defines. This will fail because the executing assembly somehow doesn't know this InterfaceX type (cast fails). Solution would be to include InterfaceX into some assembly that both the plugins and the executing assembly reference but this should mean a recompile for every new provider/consumer pair and is highly undesireable.

有什么建议吗?

长版:

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

I'm developing some sort of generic service that will use plugins for achieving a higher level of re-usability. The service consists of some sort of Observer pattern implementation using Providers and Consumers. Both providers and Consumers should be plugins for the main application. Let me first explain how the service works by listing the projects I have in my solution.

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

Project A: A Windows Service project for hosting all plugins and basic functionality. A TestGUI Windows Forms project is used for easier debugging. An instance of the ServiceProcessor class from Project B is doing the plugin related stuff. The subfolders "Consumers" and "Providers" of this project contains subfolders where every subfolder holds a consumer or provider plugin assebly respectively.

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

Project B: A Class library holding the ServiceProcessor class (that does all plugin loading and dispatching between plugins, etc), IConsumer and IProvider.

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

Project C: A Class library, linked to project B, consisting of TestConsumer (implementing IConsumer) and TestProvider (implementing IProvider). An additional interface (ITest, itself derived from IProvider) is implemented by the TestProvider.

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

The goal here is that a Consumer plugin can ask the ServiceProcessor which Providers (implementing at least IProvider) it has). The returned IProvider objects should be casted to the other interface it implements (ITest) in the IConsumer implementation so that the consumer can hook event handlers to the ITest events.

当项目 A 启动时,包含消费者和提供者插件的子文件夹被加载.以下是我目前遇到并试图解决的一些问题.

When project A starts, the subfolders containing the consumer and provider plugins are loaded. Below are some problems I've encountered so far and tried to solve.

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

The interface ITest used to reside in Project C, since this only applies to methods and events TestProvider and TestConsumer are aware of. The general idea is to keep project A simple and unaware of what the plugins do with each other.

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

With ITest in project C there and code in the Initialize method of the TestConsumer that casts the IProvider to ITest (this whould not fail in a single class library itself when an object implementing ITest is known as an IConsumer object) an invalid casting error would occur. This error can be solved by placing the ITest interface into project B that is referenced by project A as well. It is highly unwanted though since we need to recompile project A when a new interface is build.

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

I tried to put ITest in a single class library referenced by project C only, since only the provider and consumer need to be aware of this interface, but with no success: when loading the plugin the CLR states the referenced project could not be found. This could be solved by hooking on the AssemblyResolve event of the current AppDomain but somehow this seems unwanted as well. ITest went back to Project B again.

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

I tried to split project C into a separate project for the consumer and provider and both load the assemblies which itself work well: both assemblies are resident in the Assemblies collection or the current AppDomain: Assembly found: Datamex.Projects.Polaris.Testing.Providers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=2813de212e2efcd3 Assembly found: 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,Culture=neutral,PublicKeyToken=2813de212e2efcd3

Since the Consumer uses the Provider a reference was made from the Consumer to the Provider. Now the AssemblyResolve event fired again stating it needs the following file: AssemblyName=Datamex.Projects.Polaris.Testing.Providers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=2813de212e2efcd3

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

My questions: Why is this? This file is already loaded right? Why is the cast from IProvider to some interface I know it implements impossible? This is probably because the executing program itself doesn't know this interface, but can't this be loaded dynamically?

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

My ultimate goal: Consumer plugins ask the ServiceProcessor which Providers it has that do implement Interface x. The providers can be casted to this interface x, without executing assembly being aware of interface x.

有人可以帮忙吗?

提前致谢,埃里克

推荐答案

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

I just tried to recreate your solution as best as I can, and I have no such issues. (Warning, lots of code samples follow....)

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

First project is the application, this contains one class:

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

显然,这可以大大整理.

Obviously this can be tidied up enormously.

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

Next project is the shared library which contains the following three interfaces:

public interface ILoader
{
    IProvider RequestProvider(Type providerType);
}

public interface IConsumer
{
    void Initialize(ILoader loader);
}

public interface IProvider
{
}

最后是带有这些类的插件项目:

Finally there is the plugin project with these classes:

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 被复制到应用程序的搜索目录 - 但它们并不相互引用.

Both the application and the plugin projects reference the shared project and the plugin dll is copied to the search directory for the application - but they don't reference one another.

当构建 PluginLoader 时,它会找到所有 IProvider,然后创建所有 IConsumers 并调用它们的 Initialize.在初始化内部,消费者可以从加载器请求提供者,在此代码的情况下,一个 TestProvider 被构造并返回.所有这些都对我有用,对程序集的加载没有花哨的控制.

When the PluginLoader is constructed it finds all the IProviders then creates all the IConsumers and calls Initialize on them. Inside the initialize the consumer can request providers from the loader and in the case of this code a TestProvider is constructed and returned. All of this works for me with no fancy control of the loading of assemblies.

这篇关于具有插件之间共享接口的 C# 插件架构的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆