Jim's Blog Ramblings about novels, comics, programming, and other geek topics

20Feb/092

How to write an application supporting plug-ins

Google AdSense

In one of my current projects, I'm designing web services that may interact with various different third-party software. Right now, I'm just integrating one third-party API into the web services. In the future, there may be a few different third-party APIs integrated into the system.

I didn't want to include all of the third-party libraries as dependencies of my web service application and some of the data providers may be written by other developers, who I really don't want messing around with my source code. So this lead me to the plug-in architecture design.

As an example, a client may submit a request information from a specific data source that has been configured in the host web service to use a specific plug-in. Of course, there's exception and error handling built into the host web service to make sure every request is validated and verified.


Step 1: Build your plug-in interface as a separate assembly (library)

By packaging your interface as a separate assembly, this allows you to give the plug-in developers your interface assembly without giving them all of the other assemblies and decencies of your host application. Your plug-in interface assembly should contain all of the classes required to implement your plug-in.

You should try to limit dependencies on other libraries. However, if you must add another dependency, you should make sure that the dependency only contains the relevant classes needed to implement your plug-in.

I won't bother with the details on how to code a library containing an interface, but for this example let's use the following interface as an example.

public interface IMyPlugin
{
    string Name
    {
        get;
    }

    int DoSomething(int x, int y);
}

 

Step 2: Update your host application to load your plug-in

If you're not familiar with loading assemblies via reflection, then this part will be the new information for you.

Step 2.1: Plug-in manager class

First, we'll create the class that will load and retrieve the plug-ins. Below is the basic idea of the class and you'll probably deconstruct this into multiple private methods. For this example, this will be the basic coding structure.

internal class PlugInManager
{
    private Dictionary<string, IMyPlugin> plugins = null;
    private string GetPluginPath() 
    { 
        // return directory name 
    } 
    internal void LoadPlugins()
    {
        // coding goes here
    }
    internal IMyPlugin GetPlugIn(string plugInName) 
    {
        // coding goes here 
    } 
}
Step 2.1: Write handler to resolve dependencies of plug-ins (optional)

This step is optional. However, if your plug-ins have dependencies other than the interface assembly (created in step 1), then you'll need to code this. Add the following code to the LoadPlugins method.

AppDomain.CurrentDomain.AssemblyResolve +=
    new ResolveEventHandler(PlugInManager_AssemblyResolve);

Now, let's code the assembly resolution handler method.

Assembly PlugInManager_AssemblyResolve(object sender, ResolveEventArgs args)
{
    // This handler is called only when the common language runtime 
    // tries to bind to the assembly and fails.        
    Assembly resolvedAssembly = null;             
    string pluginsPath = GetPluginPath();              
    string unresolvedAssemblyName = 
              args.Name.Substring(0, args.Name.IndexOf(","));
    string resolvedAssemblyPath = 
              Path.Combine(pluginsPath, unresolvedAssemblyName + ".dll");

    if (!string.IsNullOrEmpty(resolvedAssemblyPath) 
              && File.Exists(resolvedAssemblyPath))
    {
        //Load the assembly from the specified path.
        resolvedAssembly = Assembly.LoadFile(resolvedAssemblyPath);
        //Return the loaded assembly.
        return resolvedAssembly;
    }

    return null;
}

Step 2.2 Load each plug-in in the plug-ins directory

You'll need to get all of the libraries in the plug-in directory and then inspect them to make sure the assembly implements the plug-in interface. If the assembly can be loaded, then we'll load it and add it to the Dictionary we created above.

string assemblyPath = GetPluginPath();
plugins = new Dictionary<string, IMyPlugin>();
string[] pluginFiles = Directory.GetFiles(assemblyPath, "*.DLL");

for (int i = 0; i < pluginFiles.Length; i++)
{
    string pluginAssemblyFullPath = Path.GetFullPath(pluginFiles[i]);
    Assembly pluginAssembly = Assembly.LoadFile(pluginAssemblyFullPath);

    if (pluginAssembly != null)
    {
        Type[] types = pluginAssembly.GetTypes();

        foreach (Type t in types)
        {
            if (typeof(IMyPlugin).IsAssignableFrom(t))
            {
                IMyPlugin myPlugIn = (IMyPlugin)Activator.CreateInstance(t);
                plugins.Add(myPlugIn.Name, myPlugIn);
            }
        }
    }
}

You'll note that for this example, we're not assigning a host application property to the plug-in. If you need your plug-in to interact with the host application, then you'll need to add an interface (IMyPluginHost) to the interface library, add the appropriate code to the IMyPlugin interface to allow setting the value, and then add the appropriate in your plug-in to interact with your host via the IMyPluginHost interface.

In this example, the host application interacts with the plug-ins and the plug-ins do not interact directly with the host application.

Step 2.3 Coding the method to retrieve the plug-in

Next, we'll code the method to retrieve the plug-in from the dictionary.

internal IMyPlugin GetPlugIn(string plugInName)
{
    if (plugins.ContainsKey(plugInName))
    {
        return plugins[plugInName];
    }
    return null;
}

Step 2.4 Using the Plug-in in the host application

After all of the plug-ins have been loaded (or you can change the loading process to a more dynamic loading and unloading process), then you can access the plug-ins through the PlugInManager class's GetPlugIn method.

private int DoSomething(string plugInName, int x, int y)
{
    IMyPlugin myPlugIn = myPlugInManager.GetPlugIn(myPlugInName);

    if (myPlugIn != null)
    {
        return myPlugIn.DoSomething(x, y);
    }

    return 0;
}

 

Step 3: Code your plug-in

Finally, it's time to code your plug-in in a new project. The only requirement is that you implement the interface we created in Step 1. Once you've finished coding, compile your plug-in, locate your assembly (e.g., "/bin/debug"), and copy it to the "plugin" directory we created in Step 2.

public class MyAdditionPlugin : IMyPlugin
{
    public string Name
    {
        get { return "MyAdditionPlugin"; }
    }
    public int DoSomething(int x, int y)
    {
        return x + y;
    }
}

Step 4: Testing your plug-in

Run your application and make sure your plug-in loads properly.

kick it on DotNetKicks.com

James Welch

James Welch is a software engineer in Vermont working for a large information technology company and specializing in .NET. Additionally, he holds a Master’s Degree in Software Engineering and a Bachelor of Science Degree in Computer Science. Jim also enjoys local craft beer, comic books, and science-fiction and fantasy novels, games, and movies.

Twitter Google+ 

Comments (2) Trackbacks (0)
  1. Why not using MEF instead of doing it manually?

  2. There’s a few reasons why I didn’t consider MEF. Although, it does sounds like a great framework.

    MEF is still in beta. I’m working on a system that will go live (a real system, not a personal site and not one of those always in development and changing sites) before MEF gets out of beta.

    Secondly, why would I want to include a new dependency when we’re only talking about a dozen or so lines of code? If I was going to include a new dependency, then it needs to do something that either I don’t want to write myself, does it better than I can, or is so complicated of a requirement that it would take me longer to code it then implement another library. This didn’t fit into any of those.

    Third, I can write my interfaces specific to my application and the developers will just code to the interface rather than learn a framework that probably does x10 times more than we need. So having including a new dependency that’s an entire framework with lots of more features than needed, over complicates the host project and the future plug-in projects.


Leave a Reply

No trackbacks yet.