Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Plugin framework

4.87/5 (30 votes)
22 Oct 2014CPOL3 min read 58.7K   1.9K  
Simple framework that enables your application to dynamically discover, load and unload plugins.

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:

C#
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:

C#
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()
        {
            //TODO:
        }
    }

Next step is to create a plugin. I prefer to implement one plugin per project but there are no restrictions to do several implementations:

C#
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).

C#
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:

C#
[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:

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)