Introduction
This article introduces a framework which provides simple interface to load/unload custom plugins into your application. It is based on FileSystemWatcher
, AppDomain
and MEF
technology. The main features are:
- Any structure of folder with plugins:
- subfolders
- several plugin dlls in same folder
- several plugin implementations in single dll
- Creating, renaming, replacing, deleting plugin folder or files in the runtime
- No memory leaks during loading\reloading plugins
- Minimum additional code in the plugin implementations
- Loading plugins with parameterized constructor
- Plugin may have any regular syntax (generic methods, dynamic, events etc.)
- Plugin may have any additional dependencies
- Unhandled exception in plugin will not crash host application
Background
I like when application is flexible and dynamic. But even in 21st Century most applications requires rebooting every time you change something. Terrible waste of time, IMHO.
So I performed short investigation what do we have on market. Finally I found three ways (in .Net) how to implement plugin architecture in my applications - MEF, MAF and Reflection. But none of them covers my requirements fully:
- MEF:
- Blocks executable files which prevents change dlls in runtime
- Loads dlls in the same AppDomain. If one of plugins throw unhandled exception - entire application may down
- MAF:
- The folder with plugins should have predefined structure
- Complexity of implementation, especially on plugin implementation side
- Reflection:
- Too low level and I didn't want to reinvent a wheel
Using the code
Basically the framework monitors specified folder and all subfolders on create/change/rename/delete events. When 'create' event happens framework creates separate AppDomain
and loads plugins (that are in created folder) into this domain. For loading plugins framework uses MEF
. Also, to avoid blocking files, AppDomain creates a shadow copy
of created folder. When folder or one of its root files changes/renames/deletes framework deletes old AppDomain and creates new one with new instances of plugins. So it will be one AppDomain per folder. The only restriction here is that all objects that passes from plugin into host application and vise versa should be MarshalByRefObject
. It's needed because objects pass via AppDomain boundaries.
Let's look how it works.
First need to define a contract. This is an interface that is shared between host application and plugins. So it should be defined in a shared assembly:
public interface IPlugin : IDisposable
{
string Name { get; }
string SayHelloTo(string personName);
}
As I said the only restriction is to inherrit plugin class from MarshalByRefObject
. I don't want to scare developers who will implement this contract so I will do this in base class in the same project:
public abstract class BasePlugin : MarshalByRefObject, IPlugin
{
public BasePlugin(string name)
{
Name = name;
}
public string Name { get; private set; }
public abstract string SayHelloTo(string personName);
public virtual void Dispose()
{
}
}
Next step is to create a plugin. I prefer to implement one plugin per project but there are no restrictions to do several implementations:
using Contracts;
using System;
using System.ComponentModel.Composition;
namespace Plugin1
{
[Export(typeof(IPlugin))]
public class Plugin : BasePlugin
{
public Plugin()
: base("Plugin1")
{
Console.WriteLine("ctor_{0}", Name);
}
public override string SayHelloTo(string personName)
{
string hello = string.Format("Hello {0} from {1}.", personName, Name);
return hello;
}
public override void Dispose()
{
Console.WriteLine("dispose_{0}", Name);
}
}
}
To alow MEF to discover this plugin need to decorate it with MEF attribute 'Export
' and specify contract type. Also need to add 'System.ComponentModel.Composition
' reference to the project.
The last step is to create a host application. As an example I will use console application. Need to add following references: Contracts
(assembly with contract), Mark42
(this is plugin framework itself).
using Contracts;
using Mark42;
using System;
using System.Collections.Generic;
using System.IO;
namespace TestConsole
{
class Program
{
static void Main(string[] args)
{
var pluginsFolderPath = Path.Combine(AppDomain.CurrentDomain.SetupInformation.ApplicationBase, "Plugins");
PluginService<IPlugin> pluginService = new PluginService<IPlugin>(pluginsFolderPath, "*.dll", true);
pluginService.PluginsAdded += pluginService_PluginAdded;
pluginService.PluginsChanged += pluginService_PluginChanged;
pluginService.PluginsRemoved += pluginService_PluginRemoved;
pluginService.Start();
Console.ReadKey();
pluginService.Stop();
}
#region Event handlers
private static void pluginService_PluginRemoved(PluginService<IPlugin> sender, List<IPlugin> plugins)
{
foreach (var plugin in plugins)
{
Console.WriteLine("PluginRemoved: {0}.", plugin.Name);
plugin.Dispose();
}
}
private static void pluginService_PluginChanged(PluginService<IPlugin> sender, List<IPlugin> oldPlugins, List<IPlugin> newPlugins)
{
Console.WriteLine("PluginChanged: {0} plugins -> {1} plugins.", oldPlugins.Count, newPlugins.Count);
foreach (var plugin in oldPlugins)
{
Console.WriteLine("~removed: {0}.", plugin.Name);
plugin.Dispose();
}
foreach (var plugin in newPlugins)
{
Console.WriteLine("~added: {0}.", plugin.Name);
}
}
private static void pluginService_PluginAdded(PluginService<IPlugin> sender, List<IPlugin> plugins)
{
foreach (var plugin in plugins)
{
Console.WriteLine("PluginAdded: {0}.", plugin.Name);
Console.WriteLine(plugin.SayHelloTo("Tony Stark"));
}
}
#endregion
}
}
As you can see it is quite easy to use. Need to create PluginService
, specify desired contract and where to look for instances. There are two additional parameters: a wildcard for files to monitor and a flag that anables service to monitor subfolders.
PluginService
has three events which raise when plugins added, changed or removed.
For convenience go to plugin project properties and re-direct its output path into "..\TestConsole\bin\Debug\Plugins\Plugin1\".
That's it! Now run console application and try to play with Plugin1 folder. You will see that console application receive events about plugin reloading and new instance of the plugin.
Parameterized constructor
There is a way how to write plugins with parameterized constructors. It requires a little coding.
First need to add desired constructor into plugin class and decorate it with attribute ImportingConstructor
:
[Export(typeof(IPlugin))]
public class Plugin : BasePlugin
{
[ImportingConstructor]
public Plugin(CustomParameters parameters)
: base("Plugin1")
{
Console.WriteLine("ctor_{0}", Name);
}
public override string SayHelloTo(string personName)
{
string hello = string.Format("Hello {0} from {1}.", personName, Name);
return hello;
}
public override void Dispose()
{
Console.WriteLine("dispose_{0}", Name);
}
}
Type CustomParameters
should be shared between plugin project and host application project. Also don't forget to make it MarshalByRefObject
.
Next step is to create CustomPluginService
in host application project:
using Contracts;
using Mark42;
using System.Collections.Generic;
namespace TestConsole
{
public class CustomPluginService<TPlugin> : PluginService<TPlugin>
where TPlugin : class
{
public CustomPluginService(string pluginsFolder, string searchPattern, bool recursive)
: base(pluginsFolder, searchPattern, recursive)
{
}
protected override List<TPlugin> LoadPlugins(MefLoader mefLoader)
{
CustomParameters parameters = new CustomParameters()
{
SomeParameter = 42
};
return mefLoader.Load<TPlugin, CustomParameters>(parameters);
}
}
}
The main idea here is to override LoadPlugin
method. Inside this method you may call Load
method with up to 16 parameters.
Finally, in the Main
method replace PluginService
with CustomPluginService
. That's it. Should work.
History
22.10.2014 - first version