Make a really dynamic plugin using multiple Application Domains


Note: This article assumes that you have basic knowledge of creating and using plugins. It won’t cover the whole process of plugin development. There are too many articles in the internet towards this topic to make another copy of sources 😉 I want to dwell on some specific sugar that could possibly help solving problems described below.

Yesterday I was playing with a windows service based application that was capable of dynamic loading and execution of .net assemblies. The loading and execution process was perfect except one ugly though common thing that can be found in many plugin based applications. After the assembly was loaded and all the required static methods executed successfully I couldn’t update it or delete. This is obvious behavior when loading assemblies in .net by means of "type loaders", usage of "Assembly.Load()" etc. and shouldn’t be regarded as bug.

The "Assembly.Load" function loads assembly to current application domain so you won’t be able to do a lot untill your plugin host application is unloaded. Event don’t think of hot changes of assemblies with your host running. To override this you should move from kid’s type loaders to multiple application domains 🙂 I’ll show how to do that.

Additionally you will see how the multiple AppDomains approach helps solving problems with single threaded applications like XNA games. XNA Framework based game can be easily converted to assembly, but it cannot be simply executed by the plugin host. It doesn’t assume being loaded from another thread, you cannot apply all the stuff from your favorite own plugin framework. It also needs the power of multiple AppDomains plus some minor changes in your game code.

So let’s start implementation part…

First we define some dummy IPlugin interface for external assemblies. It will be placed to a PluginLib.dll assembly common either for host application and external plugins.

IPlugin.cs

using System;

namespace PluginLib
{    
    public interface IPlugin : IDisposable
    {
        string Name { get; }
        void Run(object param);
    }
}

 

Here we have a name of the plugin for future UI integration purposes and execution call with some parameter. You can use your own existing IPlugin interface only don’t forget to inherit IDisposable interface.

Additionally we implement our own custom exception for future purposes. It can also be modified to suit your needs.

PluginException.cs

using System;

namespace PluginLib
{
    [Serializable]
    public class PluginException : Exception
    {
        public PluginException() { }
        public PluginException(Exception ex) : base("Plugin Exception", ex) { }
    }
}

The most important thing I want to dwell is the plugin host. The significant part of the projects will be placed in it. For the details for using "AppDomainManager" and "AppDomainSetup" you should refer to MSDN.

Note that your hosting logic class MUST inherit MarshalByRefObject so the host application can create an object of the PluginHost inside other AppDomains, also it should implement IDisposable to cleanup any resources.

We can definitely split the process of plugin execution in the folowing steps:

1. Create and setup new Application Domain for the required assembly binding a .config file if found

2. Add domain to the internal collection of loaded domains.

3. Load the required assembly from dedicated appdomain created

4. Initialize a plugin from assembly

5. Do whatever you want, in my sample I will call "Run(object param)" method to start doing something

 

1-2. Creating and setting up new Application Domain, loading domain to internal collection

Assume that we already defined a global collection variable for storing all the application domains loaded

// The list of loaded application domains
List<AppDomain> _appDomains = new List<AppDomain>(); 

Our host will contain only one public method called RunPlugin accepting one string argument as the physical path of the plugin assembly to load

 

/// <summary>
/// This function is what the Host Application is going to call.
/// </summary>
/// <param name="path">The plugin (*.dll) file</param>
public void RunPlugin(string path)
{
    //Creating the appdomain manager
    AppDomainManager manager = new AppDomainManager();
    //Check if there is any *.dll.config file
    string configFileName = string.Format("{0}.config", path);
    AppDomainSetup setup = new AppDomainSetup();
    //Enable shadow copying, so the Plugin files are not locked
    setup.ShadowCopyFiles = "true";
    setup.LoaderOptimization = LoaderOptimization.MultiDomain;
    //if the config file exists, load it into the appdomain
    if (File.Exists(configFileName))
        setup.ConfigurationFile = configFileName;
    //Creating the AppDomain & adding a reference to it in the _appDomains collection.
    AppDomain domain = manager.CreateDomain(String.Format("AD-{0}", _appDomains.Count), null, setup);
    _appDomains.Add(domain);
    /*
     * This important.
     * Here we are initiating an instance of PluginHost inside the new AppDomain.
     * this instance will give us control from the host appdomain to load & run the plugins inside the new appdomain
     */
    PluginHost remoteHost = domain.CreateInstanceAndUnwrap(Assembly.GetAssembly(typeof(PluginHost)).FullName, typeof(PluginHost).ToString()) as PluginHost;

    // Here we run every plugin in a separate thread
    ThreadPool.QueueUserWorkItem(
        delegate(object state)
        {
            try
            {
                //calling the PluginHost object created in the other appdomain.
                remoteHost.Launch(path, domain);
            }
            catch (Exception ex)
            {
                _appDomains.Remove(domain);
                AppDomain.Unload(domain);
            }
        });
} 

The code above is well commented, so I’d like to dwell on "Launch" method of our PluginHost class

 

3-5. Plugin execution from dedicated AppDomain

/// <summary>
/// this function will load the specified assembly file in the specified AppDomain, then it is going to run any class that implements IPlugin inside.
/// </summary>
/// <param name="assemblyPath">The assembly (*.dll) file which contains the plugin(s)</param>
/// <param name="domain">The appdomain in which the assembly is going to be loaded</param>
void Launch(string assemblyPath, AppDomain domain)
{
    //Loading the assembly file into the appdomain
    Assembly pluginAssembly = domain.Load(AssemblyName.GetAssemblyName(assemblyPath));
    domain.AssemblyResolve += delegate(object sender, ResolveEventArgs args)
    {
        AppDomain d = sender as AppDomain;
        string path = Path.Combine(@"C:PluginsReferences", args.Name.Split(',')[0] + ".dll");
        return d.Load(path);
    };

    RaiseEvent(String.Format("Hello from domain {0}", domain.FriendlyName));
    //Searching for Plugin types inside the loaded assembly
    foreach (Type type in pluginAssembly.GetTypes())
    {
        if (!type.IsClass) continue;

        if (type.FindInterfaces(delegate(Type t, object filter) { return t == filter as Type; }, typeof(IPlugin)).Length > 0)
        {
            //Using block will make sure that the plugins will run cleanup code after execution
            using (IPlugin plugin = pluginAssembly.CreateInstance(type.ToString()) as IPlugin)
            {
                plugin.Run("Here we place some params for the external plugin");
            }
        }
    }
}

This part should be definitly configured to suit you needs. For quick implementation purposes I’ve hardcoded some parts.

Upon loading our plugin assembly we might encounter references to third party libraries not known to our host application. This must be resolved somehow. We attach to AssemblyResolve event of our dedicated AppDomain to reference a folder where all the needed .dll files would be present. Assume that this is a separate folder for all the third party stuff. Here I’ve defined "C:PluginsReferences" for that purposes.

You can also implement some event raising functionality to inform your application about something. Here you can see the call of "RaiseEvent" method. It is as simple as possible:

event PluginEventHandler _pluginEvent;
public event PluginEventHandler PluginChange
{
    add { _pluginEvent += value; }
    remove { _pluginEvent -= value; }
}

private void RaiseEvent(string message)
{
    if (_pluginEvent != null)
        _pluginEvent(message);
}

And at last we use the old fashioned type loader as it is always advised in the internet 😉 creating instances and executing methods.

 

Disposing Plugin Host

We should also take care of releasing our AppDomain collection. The disposing is too boring to comment

#region IDisposable Members

public void Dispose()
{
    _appDomains.ForEach(
        delegate(AppDomain domain)
        {
            if (!domain.IsFinalizingForUnload())
                AppDomain.Unload(domain);
        });
}

#endregion

 

How it is used in your application

1. Preparing plugin. Turning application to plugin for testing purposes.

1. Prepare any simple Windows Forms Application, put some controls on it and so on…

2. Add reference to our PluginLib assembly

3. Open project properties and change the output type to "Class Library"

 

In the default Program.cs file you might see the following common picture

static class Program
{
    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new Form1());
    }
}

Let’s change it implementing some king of plugin loader

//static class Program
//{
//    /// <summary>
//    /// The main entry point for the application.
//    /// </summary>
//    [STAThread]
//    static void Main()
//    {
//        Application.EnableVisualStyles();
//        Application.SetCompatibleTextRenderingDefault(false);
//        Application.Run(new Form1());
//    }
//}

public class SimpleForm : IPlugin
{
    #region IPlugin Members

    public string Name
    {
        get { return "SimpleForm"; }
    }

    public void Run(object param)
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new Form1());
    }

    #endregion

    #region IDisposable Members

    public void Dispose()
    {
    }

    #endregion
}

It’s very simple, isn’t it? 🙂

2. Implementing simple Plugin Host Application

Create folder "C:Plugins" and put there your WindowsApplication1.dll (see code above) and PluginLib.dll

I’ll take the basic console application to dwell only on PluginHost implementation if you don’t mind 😉

Don’t forget to add reference to our common PluginLib assembly

public static void Main(string[] args)
{
    //Using block will make sure that the cleanup code of PluginHost is executed
    using (PluginHost host = new PluginHost())
    {
        host.PluginChange += new PluginEventHandler(host_PluginChange);
        host.RunPlugin(@"C:PluginsWindowsApplication1.dll");
        Console.ReadKey();
    }
}

static void host_PluginChange(string message)
{
    Console.WriteLine(message);
}
It's up you what the logic of your hosting application will be. 
Here you create a new host, link to the simple host event and finally execute the plugin. 
In this case you will see the application designed.
Note: When you plugin is executed and you see windows form you can freely delete the plugin, change it, update and so on.
Source code for the article

Advertisements

3 thoughts on “Make a really dynamic plugin using multiple Application Domains

  1. Hi thank you for your code but I get a stackoverflow !
    I explain myself. I’ve an app (winform) loading plugins (usercontrols) but linked to many other dll (business, data…) so when I want to update my plugins, I try to call your code because my “old” code was

    Assembly assembly = Assembly.LoadFrom(file, thisAssembly.Evidence);
    or Assembly assembly = Assembly.LoadFile(file, thisAssembly.Evidence);

    then Type[] typeArray = null;
    try { typeArray = assembly.GetExportedTypes(); }
    throws some time file not found exeption…

    so I tried your code:

    first I had to change this

    string path = args.Name;
    if (args.Name.IndexOf(‘,’) > 0)
    path = Path.Combine(@”[myPath]\Plugins\References”, args.Name.Split(‘,’)[0] + “.dll”);

    in the void Launch(string assemblyPath, AppDomain domain) method
    because first time, its Ok (good dll name) but after I get the path of my dll, no longer the name??

    Do you have an idea ? please ?
    Davy

  2. here is the full test

    domain.AssemblyResolve += delegate(object sender, ResolveEventArgs args)
    {
    AppDomain d = sender as AppDomain;
    string path = args.Name;
    if (args.Name.IndexOf(‘,’) > 0)
    path = Path.Combine(@”[myPath]\Plugins\References”, args.Name.Split(‘,’)[0] + “.dll”);
    try { return d.Load(path); }
    catch { return null; }
    };

    RaiseEvent(String.Format(“Hello from domain {0}”, domain.FriendlyName));
    //Searching for Plugin types inside the loaded assembly
    Type[] tt = pluginAssembly.GetTypes();
    foreach (Type type in tt)
    {…

    first I get “CS.ReusableForms, Version=1.0.0.0, Culture=neutral, PublicKeyToken=75991014499c4d87”

    then I get [myPath]\\Plugins\\References\\CS.ReusableForms.dll

    and it’s looping all the time ?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s