Click here to Skip to main content
Click here to Skip to main content
Articles » Languages » C# » General » Revisions
 

A simple plug-in engine using Reflection

, 17 Jun 2012 CPOL
Rate this:
Please Sign up or sign in to vote.
This article describes how to create and use configurable plug-ins in your application.
This is an old version of the currently published article.

Introduction

In fact everyone now knows what a plugin is. A plug-in is a set of software components that extends functionality of an application. For example, plug-ins are often used in different multimedia players to extend formats that the player can play.

Background

With the release of the .NET 4.0 platform we've got the Managed Extensibility Framework (MEF) – a quite decent extension engine.

But sometimes (though seldom) a customer limits the version of the framework used. Of course you could ask Google to search for ready solutions for implementing plugins. For example I've seen the Plux.NET platform for building plugin systems but I didn't try to review it in details.

So we've got a task to create the solution that would allow your application to extend its functionality without recompilation. An approximate plan of working with plugins is as follows:

  • There is an application that supports adding plugins.
  • You have added some plugin files to the special application folder.
  • The application must initiate loading of the plugins. For this purpose the application must be restarted or use an explicit method call, e.g., LoadPlugins().
  • Enjoy the extended function set!

Creating base classes for our future plugins

First of all, let's implement the PluginBase class – as you can understand from its name, it is the base class for our future plugins.

Create a new class library project in Visual Studio named PluginBase and add a file with the next code to the project:

/// <summary>
/// Base class for a plugin
/// </summary>
public abstract class PluginBase
{
    /// <summary>
    /// Plugin's name
    /// </summary>
    public virtual string Name { get; protected set; }

    /// <summary>
    /// Plugin's description
    /// </summary>
    public virtual string Description { get; protected set; }

    /// <summary>
    /// Plugin's configuration
    /// </summary>
    public virtual ConfigurationBase Configuration { get; set; }

    /// <summary>
    /// Default constructor
    /// </summary>
    public PluginBase()
    {
        Name = "Unnamed plugin";
        Description = "No description";
    }

    /// <summary>
    /// Does some work
    /// </summary>
    public abstract void Run();
}

Our simple plugins will contain just one clear method – Run(). You can implement any method you want. I said "clear" method because there are three properties in this class – Name, Description and Configuration.

The Run() method is abstract that's why you need to implement this method in your plugin. Properties are just virtual so you can override them in your descendant plugin.

It would be great to have configurable plugins. And it would be much greater if each plugin's configuration would be stored in a separate section of the app.config file of the host application.

So let's do it. Here is the abstract class ConfigurationBase, it's inherited from the ConfigurationSection class (don't forget to add a reference to the System.Configuration namespace).

public abstract class ConfigurationBase : ConfigurationSection
{
    /// <summary>
    /// Opens the configuration section with the specified type and name
    /// </summary>
    /// <param name="sectionName">Configuration section's name</param>
    /// <param name="configPath">Configuration file's path</param>
    /// <returns>Instance of the configuration section's class</returns>
    public static T Open<T>(string sectionName, string configPath) where T : ConfigurationBase, new()
    {
        T instance = new T();
        if (configPath.EndsWith(".config", StringComparison.InvariantCultureIgnoreCase))
            configPath = configPath.Remove(configPath.Length - 7);
        try
        {
            Configuration config = ConfigurationManager.OpenExeConfiguration(configPath);
            /* section not found */
            if (config.GetSection(sectionName) == null)
            {
                config.Sections.Add(sectionName, instance);

                foreach (ConfigurationProperty p in instance.Properties)
                    ((T)config.GetSection(sectionName)).SetPropertyValue(p, p.DefaultValue, true);

                config.Save();
            }
            else
                /* section already exists */
                instance = (T)config.Sections[sectionName];
        }
        catch (ConfigurationErrorsException)
        {
            if (instance == null)
                instance = new T();
        }
        return instance;
    }
}

The ConfigurationBase class contains one generic method public static T Open<T>(string sectionName, string configPath) where T : ConfigurationBase, new(). This static method allows you to open the application configuration file and read the configuration section of the specified type and with the specified name. There will not be configuration section in the file when you first time load the plugin. That's why this method will add the section to the configuration file and after that you will be able to modify the configuration properties. Anyway the method will return the instance of the configuration section's class.

You may have noticed the line if (config.GetSection(sectionName) == null) is highlighted. Please take in mind that line. We‘ll come back to it later.

Creating the plugin!

Now it's time to create the plugin. Open a new class library project (let's call it ShowConsolePlugin) in Visual Studio and add a reference to the previously created project that contains the PluginBase and ConfigurationBase classes. Add two files in the project.

ShowConsolePluginConfiguration.cs:
public sealed class ShowConsolePluginConfiguration : PluginBase.ConfigurationBase
{
    /// <summary>
    /// Message to show
    /// </summary>
    [ConfigurationProperty("Message", DefaultValue = "Hello from ShowConsolePlugin")]
    public string Message
    {
        get
        {
            return (String)this["Message"];
        }
        set
        {
            this["Message"] = value;
        }
    }
}

This class is inherited from the ConfigurationBase class of the PluginBase assembly and has the property Message which actually is a configuration property.

ShowConsolePlugin.cs:
public sealed class ShowConsolePlugin : PluginBase.PluginBase
{
    public ShowConsolePlugin()
    {
        Name = "ShowConsolePlugin";
        Description = "ShowConsolePlugin";
    }

    public override void Run()
    {
        Console.WriteLine((Configuration as ShowConsolePluginConfiguration).Message);
    }
}

The Name and Description properties are set in the default constructor of the plugin. Don't forget about the Configuration property of the PluginBase class? We convert its type to the ShowConsolePluginConfiguration type in the overridden method Run() which writes the Message value to the system console. Do not worry the value of the Configuration property is null at this moment. We'll take care about this later.

The simplest plugin is ready!

Creating the demo app

The easiest part is over. Now we'll create a demo app that would load plugins from the specified folder. Open Visual Studio and add a new console application project named PluginDemo to the solution. First we must add a file with the PluginManager class:

internal sealed class PluginManager
{
    /// <summary>
    /// Plugins directory
    /// </summary>
    private const string pluginDir = "Plugins";

    /// <summary>
    /// Dictionary that contains instances of assemblies for loaded plugins
    /// </summary>
    public Dictionary<Assembly, PluginBase.PluginBase> Plugins { get; private set; }
    ...
}

There is the pluginDir constant in the code above. It defines a folder name where the plugins are stored. Also you can see the Plugins property. Its get method returns an instance of Dictionary<Assembly, PluginBase.PluginBase> which is used to store plugin classes as values and plugin assemblies as keys.

Now let's add the method for loading plugins:

/// <summary>
/// Loads a plugin
/// </summary>
/// <param name="FullName">Full path to a plugin's file</param>
/// <returns>"true" after successful loading, else "false"</returns>
public bool LoadPlugin(string FullName)
{
    Assembly pluginAssembly;
    try
    {
        pluginAssembly = Assembly.LoadFile(FullName);
    }
    // Can't load the assembly's file
    catch (Exception ex)
    {
        Log.Write(ex.Message, System.Diagnostics.EventLogEntryType.Error);
        return false;
    }
    try
    {
        Type t = pluginAssembly.GetTypes().FirstOrDefault(x => x.BaseType == typeof(PluginBase.PluginBase));

        PluginBase.PluginBase plugin = (PluginBase.PluginBase)Activator.CreateInstance(t);
        Plugins.Add(pluginAssembly, plugin);
        
        Log.Write(String.Format("Plugin has been loaded: {0}, {1}.", plugin.Name, plugin.Description));
        try
        {
            plugin.Configuration =
                (PluginBase.ConfigurationBase)typeof(PluginBase.ConfigurationBase)
                .GetMethod("Open").MakeGenericMethod(pluginAssembly.GetTypes()
                .FirstOrDefault(x => x.BaseType == typeof(PluginBase.ConfigurationBase)))
                .Invoke(null, new object[] { Path.GetFileNameWithoutExtension(FullName), 
                              Assembly.GetCallingAssembly().Location });
        }
        // There is not a configuration section's class in the plugin or an error occured during its loading
        catch (Exception ex)
        {
            Log.Write(String.Format("Can't load configuration for the plugin {0}: {1}", 
              pluginAssembly.GetName().Name, ex.Message), System.Diagnostics.EventLogEntryType.Error);
            // Working with default configuration
        }
        return true;
    }
    catch (Exception ex)
    {
        Log.Write(ex.Message, System.Diagnostics.EventLogEntryType.Error);
        return false;
    }
}

We use the static LoadFile method of the Assembly class here. After loading of an assembly we search for the first class derived from PluginBase in it – it is the main plugin class. Then we create an instance of the main plugin class using the Activator.CreateInstance method and add the created instance to the Plugins dictionary with a key equal to the assembly that contains the plugin class.

Let's try to load the configuration section of the plugin in the same way. We can use the generic method Open from the ConfigurationBase class:

plugin.Configuration =
        (PluginBase.ConfigurationBase)typeof(PluginBase.ConfigurationBase)
        .GetMethod("Open").MakeGenericMethod(pluginAssembly.GetTypes()
        .FirstOrDefault(x => x.BaseType == typeof(PluginBase.ConfigurationBase)))
        .Invoke(null, new object[] { Path.GetFileNameWithoutExtension(FullName), 
                Assembly.GetCallingAssembly().Location });

If the plugin assembly has a definition for a descendant of ConfigurationBase then it will be loaded. And we can use its configuration properties in the Run() method code no more worrying about null values.

You may have noticed the Log.Write(...) method above. Do not panic, this method only writes messages to the Windows Event Log and I will not show its code in the article.

Now let's modify a default constructor of the PluginManager class:

public PluginManager()
{
    Plugins = new Dictionary<Assembly, PluginBase.PluginBase>();
    
    AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;

    string path = Path.Combine(Path.GetDirectoryName(Assembly.GetCallingAssembly().Location), pluginDir);
    if (!Directory.Exists(path))
    {
        Log.Write(String.Format("Directory {0} not found. Plugins haven't been loaded!", pluginDir), 
                  System.Diagnostics.EventLogEntryType.Warning);
    }
    else
    {
        try
        {
            foreach (FileInfo f in new DirectoryInfo(path).GetFiles("*.dll"))
                if (!LoadPlugin(Path.Combine(path, f.Name)))
                    Log.Write("Can't load the plugin " + f.Name, System.Diagnostics.EventLogEntryType.Warning);
        }
        catch (SecurityException)
        {
            Log.Write("Can't get access to the directory " + pluginDir, System.Diagnostics.EventLogEntryType.Error);
        }
    }
}

Do you remember the highlighted line from the ConfigurationBase class?

When you call the config.GetSection(sectionName) you implicitly deserialize the configuration section of the type specified in the configuration file. That type is defined in the plugin assembly we've just loaded. Unfortunately the assembly was loaded with a help of the LoadFile method without any context and cannot be accessible.

We could try to use LoadFrom instead of LoadFile but according to MSDN: If an assembly is loaded with LoadFrom, and later an assembly in the default load context tries to load the same assembly by display name, the load attempt fails. This can occur when an assembly is deserialized.

That's why we need to manually resolve that assembly. Gladly we have the AppDomain.AssemblyResolve event which we can handle.

private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
    return Plugins.Keys.FirstOrDefault(x => x.FullName == args.Name);
}

More about the LoadXXX methods you can learn here and here.

Almost all is ready for the plugin testing. Just a little modification needed for the Program.cs file:

static void Main(string[] args)
{
    PluginManager pluginMan = new PluginManager();
    foreach (PluginBase.PluginBase p in pluginMan.Plugins.Values)
    {
        try
        {
            p.Run();
        }
        catch (Exception ex)
        {
            Log.Write(String.Format("An exception occured in the plugin {0} : {1}{2}", 
                 p.Name, ex.Message, ex.StackTrace), System.Diagnostics.EventLogEntryType.Error);
        }
    }
    Console.ReadKey();
}

Now we must create the folder named "Plugins" in the PluginDemo project. Please note the name of the folder must equals to the pluginDir constant defined in a code above. Copy ShowConsolePlugin.dll to the folder and set the "Copy to Output Directory" property to "Copy always".

Plugin file properties

Also I recommend to add this line to the ShowConsolePlugin project post-build event:

xcopy /y $(ProjectDir)$(OutDir)$(TargetFileName) $(SolutionDir)PluginDemo\Plugins\

Make sure to set right build order of projects. All plugins projects must be built before the PluginDemo project.

Testing the demo app

That was all the coding fun! We can compile and run the application.

Console app window 1

Running the application will create the file PluginDemo.exe.config. This file will contain:

<?xml version="1.0" encoding="utf-8" ?> 
<configuration>
  <configSections>
     <section name="ShowConsolePlugin" 
       type="ShowConsolePlugin.ShowConsolePluginConfiguration, ShowConsolePlugin, 
             Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" /> 
  </configSections>
  <ShowConsolePlugin Message="Hello from ShowConsolePlugin" /> 
</configuration>

Now let's change the Message attribute in the ShowConsolePlugin node:

<?xml version="1.0" encoding="utf-8" ?> 
<configuration>
  <configSections>
     <section name="ShowConsolePlugin" type="ShowConsolePlugin.ShowConsolePluginConfiguration, 
               ShowConsolePlugin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" /> 
  </configSections>
  <ShowConsolePlugin Message="Hello again!" /> 
</configuration>

Start PluginDemo.exe again and you will see

Console app window 2

Notes on the attached zip-file

The attached archive also contains second plug-in named SaveTxtPlugin. This plug-in just saves some text to the specified file. The article does not cover the process of creating SaveTxtPlugin because it is similar to that described above.

Remarks

Security

I do not pay attention to security permissions of plug-in methods in this article. In a real world application you can use classes from the System.Security namespace to set up permissions. For example the FileIOPermission class can help you with file or directory access permissions.

Performance

I've seen some great articles about performance when using the Reflection methods but really this article is not about performance.

History

  • 16.06.2012 - First inital release.

License

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

Share

About the Author

Vitaly Zhukov
Software Developer
Russian Federation Russian Federation
No Biography provided

Comments and Discussions


Discussions posted for the Published version of this article. Posting a message here will take you to the publicly available article in order to continue your conversation in public.
 
BugReflectionTypeLoadException Pinmembernivel9821-Jul-14 9:07 
Questiongood PinmemberHua Yujun30-Mar-13 0:48 
GeneralMy vote of 5 PinmemberXmen W.K.26-Sep-12 17:47 
QuestionMy vote of 5 PinmemberAl-Samman Mahmoud26-Sep-12 13:44 
QuestionIs there any security(permission) for using internet ? PinmemberXmen W.K.11-Jul-12 1:06 
AnswerRe: Is there any security(permission) for using internet ? PinmemberVitaly Zhukov28-Jul-12 4:25 
GeneralRe: Is there any security(permission) for using internet ? PinmemberXmen W.K.28-Jul-12 16:15 
GeneralRe: Is there any security(permission) for using internet ? PinmemberVitaly Zhukov26-Sep-12 10:06 
GeneralRe: Is there any security(permission) for using internet ? PinmemberXmen W.K.26-Sep-12 16:33 
GeneralRe: Is there any security(permission) for using internet ? PinmemberXmen W.K.26-Sep-12 17:42 
GeneralRe: Is there any security(permission) for using internet ? PinmemberXmen W.K.26-Sep-12 18:15 
GeneralRe: Is there any security(permission) for using internet ? PinmemberVitaly Zhukov26-Sep-12 21:37 
GeneralMy vote of 5 PinmemberMember 844697318-Jun-12 2:58 
GeneralMy vote of 5 PinmemberEtienne Divina17-Jun-12 20:50 

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

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

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.141223.1 | Last Updated 17 Jun 2012
Article Copyright 2012 by Vitaly Zhukov
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid