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
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With