A plugin, for those who don’t know, is a component that allows to extend an application without modifying its source code. The application loads plugins at runtime. In .NET, it’s pretty easy to accomplish using Reflection capabilities. The easy way is to expose interfaces in your application assemblies, that the plugin can implement and use, like:
public interface IPluginHost
{
void DoSomethingOnHost();
}
public interface IPlugin
{
void Initialize(IPluginHost pluginHost);
int DoSomething();
}
IPlugin
interface represents our plugin – all plugins implement this. IPluginHost
interface represents the host application. Plugins must often be able to interact with the host application in order to influence how its working.
From within the application, you can load the plugin assemblies and instantiate the plugins using code like the following:
using System.Reflection;
...
IList<IPlugin> pluginList = new List<IPlugin>();
foreach(string fileName in Directory.GetFiles("Plugins", "*.dll"))
{
Assembly assembly = Assembly.LoadFile(fileName);
foreach (Type pluginType in assembly.GetTypes())
{
if (!pluginType.IsPublic || pluginType.IsAbstract || pluginType.IsInterface)
continue;
Type concreteType = pluginType.GetInterface(typeof(IPlugin).FullName, true);
if (concreteType != null)
{
IPlugin plugin = (IPlugin)Activator.CreateInstance(concreteType);
plugin.Initialize(pluginHostInstance);
pluginList.Add(plugin);
break;
}
}
}
The code is simplified, but you can see the point, I hope. We start by loading every .dll file as an Assembly in our application. Then, we iterate through all the public
types in that assembly in search for a type that implements IPlugin
interface. Once we found it, we load it using Activator.CreateInstance
method, assuming that the type has a parameterless constructor. Then we initialize the plugin, giving it an instance of an IPluginHost
interface implementation.
Now this approach works fine and I’ve used it several times. However, you usually get into some problems, when you decide to add new functionality. IPlugin
interface represents what we need to know about the plugin. IPluginHost
interface represents what host application has to offer to the plugin. Those requirements might change quite often, so you will, most likely, make changes to those interfaces. Suppose that we want to allow our plugins to show a message box through its host. That’s easy, we just add a corresponding method to the IPluginHost
interface:
public interface IPluginHost
{
void DoSomethingOnHost();
void ShowMessageBox(string text);
}
That’s where it all breaks.
Once we release our application and plugins have started to be developed by other people, we should not make changes to the core interfaces, that the plugins depend upon. The thing is, that when you change some interface on the host, the plugin cannot be loaded, because of interface mismatch. So we no longer have backwards-compatibility. How do we extend the plugin subsystem then?
The way I implement plugins now is a bit different. The goal is to keep the core interfaces unchanged. These must be very stable. How do you allow plugins to use new features of your application then?
That’s quite easy. Instead of having a single interface, like IPluginHost
– that is almost sure to become a “dependency magnet” or, in other words, a God object, as we add more features to the host application – I utilize the Interface Segregation Principle. It says: “Clients should not be forced to depend on interfaces that they do not use”. So instead of one, big bloated interface we have many small ones. Whenever we decide to add some new functionality to our host application, we add a corresponding interface that plugins can use. In the case of the message box requirement, that I’ve shown above, we declare this:
public interface IMessageBoxHost
{
void ShowMessageBox(string message);
}
Now, we have to somehow pass the interface to the plugin. Since we can’t modify IPlugin
’s declaration (we can’t just overload Initialize
method, since that would break backwards compatibility too), we need another way to do this. What I chose to use is constructor injection. We can detect what arguments the constructor of IPlugin
implementation have and pass what it needs. For example, if we have a plugin like:
public class Plugin: IPlugin
{
public Plugin(IMessageBoxHost messageBoxHost)
{
}
}
... we pass an instance of IMessageBoxHost
implementation when instantiating the plugin:
(IPlugin)Activator.CreateInstance(concreteType, new object[] { messageBoxHostInstance })
That way, we only get what we need. We can also use any of the Dependency Injection frameworks to instantiate the plugin and fill dependencies for us.
Of course, if you spot some bug in the interface or have to change it for some other reason, the problem remains the same – changing interface will break plugins. But it’s also easier to solve – we can add a new, fixed, interface, say IMessageBoxHostEx
, and mark the old one as obsolete, thus maintaining backwards compatibility and warning users to let go of the previous one.
CodeProject