Click here to Skip to main content
15,881,559 members
Articles / Programming Languages / C#
Article

Provide plug-in support in your own applications

Rate me:
Please Sign up or sign in to vote.
4.44/5 (12 votes)
4 May 2012CPOL6 min read 33K   460   56   23
A tutorial showing how to add plug-in support to your own applications.

Introduction

This tutorial will show how to provide plug-in support in your own applications using plain .NET technologies like interfaces and Reflection.

Background

I think you have already used an application providing plug-in support. The variety of such applications is pretty large - e.g., graphic programs (like GIMP, Adobe Photoshop), text editors (like Notepad++), etc. How could you achieve to provide such support, too - enabling other developers to extend the possibilities of your own applications with their own code?

The solution

To provide Plug-In support, we use basic technologies like interfaces and Reflection. At the end of this tutorial there will be a very brief sample application, to list and call the available interfaces.

Setting up the project

The first thing we have to do is to create a new project. I chose "Console Application" as type and set its name to "DBB.PluginsTest". Leave this project as is so far.

Now we need to create our interface that enables us to gain access to plug-ins and call the plug-in's methods. To achieve this, we set up another project in our solution, which is of type "Class Library", and in my case called "DBB.Plugins.Ext".

The third and last step, setting up the project, is creating our plug-in project. So we add a new project to our solution. Its type is also "Class Library" and I called it "TestPlugin1". For our testing purposes, we change the output path for this project to the output path of our "DBB.PluginsTest" folder - that assures our plug-in resides in our working directory and we do not have to copy it around for any of our tests. To change the properties, right click the "TestPlugin1" project, choose "Properties", and set the output path in the "Build" tab to the appropriate one.

Defining our plug-in interface

Because we do not know what exactly the plug-in does and its methods, properties, etc., we need to define a standard interface which is implemented by every plug-in that shall be used with our application. For our purposes, we create a very basic interface which just holds information about the plug-in's name, its version, and a method to run.

Create a new folder in the "DBB.Plugins.Ext" project called "Interfaces". In this folder, we create a new interface called IPlugin.

C#
namespace DBB.Plugins.Interfaces
{
    public interface IPlugin
    {
        /// <summary>
        /// Gets or sets the plugin's name.
        /// </summary>
        string Name { get; }

        /// <summary>
        /// Gets or sets the plugin's version.
        /// </summary>
        string Version { get; }

        /// <summary>
        /// Runs the plugin's main method.
        /// </summary>
        /// <returns>Some object.</returns>
        object Run();
    }
}

Defining our dummy plug-in

After defining our default plug-in interface, we are going to define our dummy plug-in. To do so we have to add a reference to "DBB.Plugin.Ext" - the library that holds our plug-in interface. After adding this reference, the interface IPlugin is available in the plug-in we are going to implement.

Let's add a new class called TestPlugin11 to the "TestPlugin1" project. This class implements the IPlugin interface and looks as follows:

C#
using DBB.Plugins.Interfaces;

namespace TestPlugin1
{
    public class TestPlugin11 : IPlugin
    {
        /// <summary>
        /// Does something.
        /// </summary>
        /// <returns>Some string.</returns>
        private string DoSomething()
        {
            return string.Format("{0} - Version: {1} -> {2}", Name, Version, "DoSomething()");
        }

        #region IPlugin members.

        /// <summary>
        /// Gets the plugin's name.
        /// </summary>
        public string Name
        {
            get { return "TestPlugin1.1"; }
        }

        /// <summary>
        /// Gets the plugin's version.
        /// </summary>
        public string Version
        {
            get { return "1.0.0.0"; }
        }

        /// <summary>
        /// Runs the plugin's main method.
        /// </summary>
        /// <returns>Some object.</returns>
        public object Run()
        {
            return DoSomething();
        }

        #endregion

    }
}

As you can see, IPlugin has been implemented and the DoSomething() method returns some information about the plug-in. Of course it is also possible to implement a much more complex function.

Fill the test project with life

At this point, our interface is defined and our dummy plug-in is implemented. What we still need is some possibility to find available plug-ins and call their functionality (the Run() method).

First, let's add a new folder to the "DBB.PluginsTest" project; called "PluginsHandling" and a new class PluginUtils within this new folder. Because we want to access our IPlugin interface from our test project, we have to add a reference to the "DBB.Plugins.Ext" project in the "DBB.PluginsTest" project.

The whole plug-in handling is done within just two methods:

  • Retrieving .dll files, which might contain plug-in functionality.
  • Retrieving valid plug-ins from found in .dll files.

First we implement a method that retrieves all ".dll" files of a specified path.

C#
/// <summary>
/// Gets a list of Dll files within a specified path.
/// </summary>
/// <param name="path">Path to retrieve Dll files in.</param>
/// <returns>List of strings with file names.</returns>
public static List<string> GetDllList(string path)
{
    return new List<String>(Directory.GetFiles(path, "*.dll"));
}

After that the interesting part needs to be implemented: Reading the .dll files and checking for valid plug-in functionality.

This check is done via Reflection technologies and looks as follows:

C#
/// <summary>
/// Gets a list of IPlugin objects.
/// </summary>
/// <param name="dllFiles">List of Dll files to check for plugin capabilities.</param>
/// <returns>List of IPlugin objects.</returns>
public static List<IPlugin> GetPlugins(List<string> dllFiles)
{
    List<IPlugin> retVal = new List<IPlugin>();

    foreach (string dll in dllFiles)
    {
        try
        {
            // Load dll.
            Assembly assembly = Assembly.LoadFile(dll);
            // Get class names.
            Type[] types = assembly.GetTypes();

            // Add plugin classes to plugin list.
            foreach (Type cls in types)
            {
                if (cls.IsPublic)
                {
                    // Get classe's implemented interfaces.
                    Type[] interfaces = cls.GetInterfaces();
                    foreach (Type iface in interfaces)
                    {
                        // Is current interface IPlugin?
                        if (cls.GetInterface(iface.FullName) == typeof(IPlugin))
                            retVal.Add(assembly.CreateInstance(cls.FullName) as IPlugin);
                    }
                }
            }
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }

    return retVal;
}

What are we doing here?

First we try to load each .dll file (assembly) the provided dllFiles list contains. Then we try to retrieve the class names within the single assembly and check whether the class is public. If so, we try to retrieve the interfaces the class is implementing. If an IPlugin interface is implemented, we add an instance of the current class to the list of valid plug-ins.

How to use the code?

Actually we neglected the "Program.cs" so far. We implemented everything we need to handle plug-ins but are not able to test it. Let's change this fact and open the file "Program.cs".

To our main method we add some code to retrieve the available ".dll" files and available plug-ins within them.

C#
static void Main(string[] args)
{
    // Get list of Dll files within current directory.
    List<string> dllFiles = PluginUtils.GetDllList(Directory.GetCurrentDirectory());

    // Get plugins within found Dll files.
    List<IPlugin> plugins = PluginUtils.GetPlugins(dllFiles);
}

Here we are retrieving all ".dll" files within the current working directory and trying to get valid plug-ins within these.

To list the found plug-ins we implement another method within the "Program.cs" file:

C#
/// <summary>
/// Shows a list of found plugins.
/// </summary>
/// <param name="plugins">List of IPlugin objects.</param>
private static void ShowPluginList(List<IPlugin> plugins)
{
    Console.WriteLine("Found plugins");
    Console.WriteLine("-----------------------------------------------------");
    int i = 0;
    foreach (IPlugin plugin in plugins)
        Console.WriteLine(string.Format("{0,2}) {1}", ++i, plugin.Name));
    Console.WriteLine(" x) Exit");
}

Here we are just building a small overview about found plug-ins. Now let's make this overview to a small menu, allowing us to call the appropriate plug-in. For this purpose, we move our focus again to the main method, to which the following code is added after retrieving the valid plug-ins.

C#
...

string key = string.Empty;

// Show menu and run selected plugin.
while (key.Trim().ToLower() != "x")
{
    // Show a list of found plugins.
    ShowPluginList(plugins);
    Console.Write("--> ");
    key = Console.ReadLine();

    int pk;
    int.TryParse(key, out pk);
    Console.WriteLine();
    if (pk != 0)
        Console.WriteLine(((IPlugin)plugins[pk - 1]).Run());
    Console.WriteLine();
}

...

I think the code is quite self explaining. Until another value than "x" is read from the keyboard, we call the Run() method of the list entry and print its result value to the screen.

Conclusion

If you have everything set up and implemented correctly, you should now be able to run your solution and get the following result where you can choose the plug-in to run. (Note: The screenshot shows a second implemented plug-in, which is not part of the sources presented in this tutorial, but part of the sources available to download.)

Output

Points of Interest

  • Is there really a need of "DBB.Plugins.Ext"?
  • Yes, there is! This .dll file is the window to the world and the world's window to our application. You can implement the interface in both projects but the compiler will not know to which type to cast the object. So you will run into a CastTypeException if you do not use the way with the extra .dll file.

  • Why is the plug-in handling implemented in "DBB.PluginsTest"?
  • Well, you can also implement the whole handling in "DBB.Plugins.Ext". But then you enable plug-ins to have plug-pns to have plug-pns, ...

  • Is the plug-in support safe?
  • No! Plug-in support is unsafe! You cannot control what a plug-in does. It might be named like "Rename files" and format your harddisk after it has been called.

  • What do I have to provide to a developer who wants to create a plug-in?
  • Just provide the developer the "DBB.Plugins.Ext" .dll file and the description of your IPlugin interface.

History

  • May 03, 2012 - Initial version.

License

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


Written By
CEO
Germany Germany
Developing software since 1991.

Graduated as B.Sc., M.Sc. and Ph.D.

Experienced in (in chronological order): Turbo-Pascal, C, C++, Visual Basic, JavaScript, PHP, COBOL(-ILE), CL(-ILE), Visual Basic .NET, RPG(-ILE), Java, C#.
Databases: DB2, MySQL, Oracle, PostgreSQL.

Comments and Discussions

 
GeneralMy vote of 4 Pin
Oleg A.Lukin9-May-12 5:34
Oleg A.Lukin9-May-12 5:34 
GeneralRe: My vote of 4 Pin
HiDensity9-May-12 10:13
HiDensity9-May-12 10:13 
GeneralRe: My vote of 4 Pin
Darchangel23-May-12 10:14
Darchangel23-May-12 10:14 
GeneralRe: My vote of 4 Pin
HiDensity25-May-12 4:21
HiDensity25-May-12 4:21 
QuestionA word about security Pin
Guillaume BOTTESI9-May-12 2:12
Guillaume BOTTESI9-May-12 2:12 
AnswerRe: A word about security Pin
HiDensity9-May-12 10:18
HiDensity9-May-12 10:18 
Generalgreat job Pin
charlybones8-May-12 6:05
charlybones8-May-12 6:05 
GeneralRe: great job Pin
HiDensity8-May-12 6:13
HiDensity8-May-12 6:13 
GeneralMy vote of 5 Pin
Illia Ratkevych8-May-12 0:51
Illia Ratkevych8-May-12 0:51 
GeneralRe: My vote of 5 Pin
HiDensity8-May-12 1:33
HiDensity8-May-12 1:33 
SuggestionYou can simplify GetPlugins() a little Pin
Jaguire7-May-12 11:11
Jaguire7-May-12 11:11 
GeneralRe: You can simplify GetPlugins() a little Pin
HiDensity7-May-12 11:22
HiDensity7-May-12 11:22 
SuggestionRe: You can simplify GetPlugins() a little Pin
bond.martini7-May-12 13:41
bond.martini7-May-12 13:41 
GeneralRe: You can simplify GetPlugins() a little Pin
HiDensity8-May-12 1:32
HiDensity8-May-12 1:32 
Thank you for your good and constructive feedback. Smile | :)

You are right, there is room left for some improvements - like the IEnumerable or the thing with immediate loading the found plug-ins.
I will have a look at these issues and maybe create an update within the next days, if I find any time.
QuestionExcellent article! Pin
Gravimax7-May-12 9:59
Gravimax7-May-12 9:59 
AnswerRe: Excellent article! Pin
HiDensity7-May-12 10:05
HiDensity7-May-12 10:05 
QuestionGreat article, very similar to what I once did... Pin
alcexhim7-May-12 6:27
alcexhim7-May-12 6:27 
AnswerRe: Great article, very similar to what I once did... Pin
pbalaga7-May-12 8:04
pbalaga7-May-12 8:04 
GeneralRe: Great article, very similar to what I once did... Pin
alcexhim7-May-12 8:09
alcexhim7-May-12 8:09 
GeneralRe: Great article, very similar to what I once did... Pin
pbalaga7-May-12 8:24
pbalaga7-May-12 8:24 
AnswerRe: Great article, very similar to what I once did... Pin
HiDensity7-May-12 9:39
HiDensity7-May-12 9:39 
GeneralMy vote of 4 Pin
Member 23036055-May-12 11:24
Member 23036055-May-12 11:24 
GeneralRe: My vote of 4 Pin
HiDensity5-May-12 23:33
HiDensity5-May-12 23:33 

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.