Search code examples
c#pluginsinterfacearchitectureobserver-pattern

C# Plugin Architecture with interfaces share between plugins


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

Short version:

I need some architecture for a system with provider and consumer plugins. Providers should implement interface 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 (using a ServiceProcessor) which providers implement InterfaceX and gets a List back. These IProvider objects should be cast 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). The 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 undesirable.

Any suggestions?

Long version:

I'm developing some sort of generic service that will use plugins to achieve 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. First, let me explain how the service works by listing the projects I have in my solution.

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 contain subfolders where every subfolder holds a consumer or provider plugin assembly respectively.

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

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.

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.

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.

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.

With ITest in project C there and code in the Initialize method of the TestConsumer that casts the IProvider to ITest (this should 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 which is referenced by project A as well. It is highly unwanted though since we need to recompile project A when a new interface is built.

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.

I tried to split Project C into a separate project for the consumer and provider and both load the assemblies which itself works 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

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

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?

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.

Somebody that can help?

Thanks in advance


Solution

  • 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
    {        
    }
    

    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.

    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.