Click here to Skip to main content
15,867,686 members
Articles / Programming Languages / C#

Plugin framework

, ,
Rate me:
Please Sign up or sign in to vote.
4.87/5 (30 votes)
22 Oct 2014CPOL3 min read 56K   1.9K   90   18
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)


Written By
Software Developer
Ukraine Ukraine
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Written By
Software Developer
Ukraine Ukraine
I have started programming at 18 years old during my studying at the university. For that, I was using Pascal and C and then .NET languages and tecnologies (C#, VB, ASP.NET).

I have graduated National Technical University of Ukraine "Kyiv Polytechnical Institute". At university, besides the languages described above, I learned Java and Web technologies.

Now, I am working at the international outsource company on software developer position.

Written By
Ukraine Ukraine
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionHow to load all plugins in the folder? Pin
tbc00725-Apr-17 21:34
tbc00725-Apr-17 21:34 
QuestionVS version? Pin
Member 1091554022-Nov-15 2:04
Member 1091554022-Nov-15 2:04 
QuestionHow can I create a Form for a plugin Pin
lightfinder21-Aug-15 3:32
lightfinder21-Aug-15 3:32 
AnswerRe: How can I create a Form for a plugin Pin
Member 1091554022-Nov-15 2:02
Member 1091554022-Nov-15 2:02 
QuestionPlugin development simplification? Pin
f.37-Jan-15 5:11
f.37-Jan-15 5:11 
GeneralThis make me laugh... Pin
cacingkalung16-Dec-14 22:44
cacingkalung16-Dec-14 22:44 
GeneralRe: This make me laugh... Pin
Vladyslav Chernysh17-Dec-14 0:21
Vladyslav Chernysh17-Dec-14 0:21 
QuestionPlugin in Visual Basic Pin
Andrea Lando2-Dec-14 23:43
Andrea Lando2-Dec-14 23:43 
AnswerRe: Plugin in Visual Basic Pin
Vadym Rybak2-Dec-14 23:55
professionalVadym Rybak2-Dec-14 23:55 
QuestionDynamic reload issues? Pin
Peter SQ@Work27-Oct-14 0:07
Peter SQ@Work27-Oct-14 0:07 
AnswerRe: Dynamic reload issues? Pin
Vladyslav Chernysh27-Oct-14 2:01
Vladyslav Chernysh27-Oct-14 2:01 
GeneralMy vote of 5 Pin
Prasad Khandekar24-Oct-14 5:36
professionalPrasad Khandekar24-Oct-14 5:36 
GeneralMy vote of 5 Pin
M Rayhan23-Oct-14 20:43
M Rayhan23-Oct-14 20:43 
GeneralMy Vote 5 Pin
Shemeemsha (ഷെമീംഷ)23-Oct-14 4:31
Shemeemsha (ഷെമീംഷ)23-Oct-14 4:31 
QuestionSource Code Missing... Pin
Gary Noble23-Oct-14 2:16
Gary Noble23-Oct-14 2:16 
AnswerRe: Source Code Missing... Pin
Dominick Marciano23-Oct-14 3:07
professionalDominick Marciano23-Oct-14 3:07 
GeneralRe: Source Code Missing... Pin
Vladyslav Chernysh23-Oct-14 6:16
Vladyslav Chernysh23-Oct-14 6:16 
GeneralRe: Source Code Missing... Pin
Dominick Marciano23-Oct-14 8:20
professionalDominick Marciano23-Oct-14 8:20 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.