views:

2393

answers:

5

Hi all,

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 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.

Any suggestions?

Long version:

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.

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.

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 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.

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 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

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, Erik

A: 

If you're question is, how can two unrelated assemblies share the same interface, the answer is 'you cannot' A solution is to include the interface in all assemblies, perhaps in a dll that the plugin-builders can reference, and in your loading assembly.

edosoft
The question is actually: how can 2 related assemblies (provider and consumer) share an interface (by means of a separate class lib when nescessary) that itself is derived from an interface that the executing assembly knows. The executing assembly only knows there are providers and consumers but it doesn't know they have some more detailed contract between them.
Erik Beijer
A: 

I've done something similar to what you are trying to do and as long as I had the assemblies in a place where the loader looked automatically, I didn't encounter any problems.

Have you tried putting all your assemblies in a subdirectory underneath where the exe resides? I can't remember the details now but there's a list of steps documented as to exactly where and in what order the loader looks for assemblies/types.

JohnOpincar
Hmm this might be something. The plugins are not in a subfolder of the executing assembly but in folders that can be configured. The executing assembly loads both provider and consumer plugins using Assembly.LoadFile() (and I verified they are in the current AppDomain). Somehow though a cast from IProvider to IMoreDetailedProvider cannot be made in the consumer, except when this IMoreDetailedProvider resides in a separate class library referenced by the executing assembly.
Erik Beijer
That's why I think it's probably a loader issue. Also, double check your type specification whereever you are keeping it.
JohnOpincar
+3  A: 

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.

Martin Harris
Thanks a lot. I'll dive into this and let you know!
Erik Beijer
I tried your code and it works. Thanks again.However it doesn't really differ that much from what I already made (which does not work). I'll post the solution I have made and maybe you can spot the problem since I still won't have a clue :-)
Erik Beijer
I finally figured out what I did wrong. It was only ONE single method that made the difference between a working solution and some hours or debugging:Your Assembly loading code: Assembly a = Assembly.LoadFrom(assembly.FullName);My erroneous loading code was: Assembly assembly = Assembly.LoadFile(filename);When I changed this to LoadFrom all worked well!!Thanks again for your time!
Erik Beijer
Glad I could help
Martin Harris
+1  A: 

You might find my articles useful to see a working example of a plugin framework, and how these issues are addressed by creating a common assembly holding the interfaces:

Plugins in C# Basic Tutorial:

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

Followup article, with a Generics enabled Plugin Manager Library:

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

Redth
+1  A: 

It is still in development, but is sounds like a perfect usecase for MEF (to be included in .Net 4) and used internally in VS2010.

MEF presents a simple solution for the runtime extensibility problem. Until now, any application that wanted to support a plugin model needed to create its own infrastructure from scratch. Those plugins would often be application-specific and could not be reused across multiple implementations.

Previews are already available on http://www.codeplex.com/MEF

The blog of Glen Block can also be useful.

Alex
I was going to answer that he should just read up on MEF, so I just voted up your answer instead. MEF solves the logistics of dealing with all this and their docs and articles about it cover theories of how to define and share concepts such as Interfaces. I would strongly suggest looking at MEF and not building your own.
Brian ONeil