Click here to Skip to main content
Click here to Skip to main content
 

Unloadable plugins

By , 15 Apr 2010
Rate this:
Please Sign up or sign in to vote.

Creating a plugin architecture in .NET is pretty easy. Unless you need to unload the plugins - AppDomain.Unload just does not work. Unfortunately I have found little help on this issue ~ google ~ so I decided to write this article.  

A few basics:  

  1. .NET uses application domains - kind of VM isolation in Java
  2. multiple app domains can exist into a single process
  3. code running in on app domain cannot directly access code/resources in another app domain - they are isolated.
  4. code failures in one app domain cannot affect other app domains
  5. individual assemblies cannot be unloaded, only whole app domains

In our case we will use a process containing a main domain - the main application - and another one where to load/unload plugins. It should look like this:

The main domain contains standard assemblies - mscorlib, System, etc - plus our plugins.dll module - this is the main application.

We now create another app domain in our code using:

AppDomain appDomainPluginA = AppDomain.CreateDomain("appDomainPluginA");

The domains should look like this:

We now add the assembly pluginA.dll to the newly created app domain.

Assembly pluginAassembly = appDomainPluginA.Load("pluginA");

The result of this is somehow bizarre. You would expect that appDomainPluginA would load the pluginA and that's all. Well, the result is a little bit different. The assembly pluginA also gets loaded into the main appdomain, the plugins main app domain.

This (somehow unexpected) behavior is documented. You can check it out on the MSDN at http://msdn.microsoft.com/en-us/library/36az8x58.aspx

Here is the quote from MSDN: "If the current AppDomain object represents application domain A, and the Load method is called from application domain B, the assembly is loaded into both application domains."

Well, this is all ok until we need this very important functionality: to be able to unload the plugin.

Thing is, once you load the assembly in an app domain, you cannot unload it until you unload the whole app domain. In our case it means that if we want to unload pluginA we have to unload the plugins app domain i.e. our main application - in other words, we have to kill the application. But this is certainly not what we want.

Using AppDomain.Unload in this case will not work:

//The app domain cannot be unloaded. This is by .net design.
//This is because pluginA has been loaded into AppDomain.CurrentDomain and
//cannot be released
//until AppDomain.CurrentDomain it's self is unloaded - i.e. at end of program.

AppDomain.Unload(appDomainPluginA);

So we need to find a way to load and unload an appdomain and not let the assemblies inside leak into the main app domain.

This is where MarshalByRef comes into the scene. It allows us to cross app domain boundaries. So, instead of us calling the load of the plugin from the main domain, we execute the code in the second domain.

Here is how it looks like:

AppDomain appDomainPluginB = AppDomain.CreateDomain("appDomainPluginB");

We add a class (RemoteLoader) in the appDomainPluginB that will handle the Assembly.Load, mark it as MarshalByRefObject so that we can break the domain boundaries so we can execute the code in the context of appDomainPluginB and not plugins domain.

RemoteLoader loader = (RemoteLoader)appDomainPluginB.CreateInstanceAndUnwrap("plugins",
    "plugins.RemoteLoader");

This basically means we have an instance of RemoteLoader in appDomainPluginB but we can access it in plugins app domain.

Now we let the instance from appDomainPluginB load the pluginB.dll (into appDomainPluginB, of course):

loader.LoadAndExecute("pluginB");

Now the code in RemoteLoader in appDomainPluginB loads the assembly pluginB.dll and the assembly does not leak anymore into the plugins domain.

The sample in this article uses reflection to scan for and execute the classes/types that implement the IPlugin interface defined in pluginsCommon.dll.

public interface IPlugin
 {
    string Execute(string input);
 }

RemoteLoader uses reflection to find the above mentioned types, and when found will execute the IPlugin.Execute method. This means that the IPlugin type info will actually be loaded into appDomainPluginB.

foreach (Type type in pluginAassembly.GetTypes())
{
  if (type.GetInterface("IPlugin") != null)
  {
    object instance = Activator.CreateInstance(type, null, null);
    string s = ((IPlugin)instance).Execute("test");
    Console.WriteLine("Return from call is " + s);
  }
}

So the app domains actually look like this:

When we are finished, we can use AppDomain.Unload to unload the domain:

//The app domain will be unloaded.
//This is because pluginB has been loaded into appDomainPluginB but not
//in AppDomain.CurrentDomain.
//This means that AppDomain.CurrentDomain does not link/point to pluginB.

AppDomain.Unload(appDomainPluginB);

This will now work fine, because none of the assemblies leaked into the main plugins app domain - they are nowhere linked, so they can be released.

It now looks like we have an easy way to load and unload plugins in .NET. It is simple, but the documentation on the net is unfortunately very scarce on this.

I hope you enjoyed the article and please share it if you found it usefull.

PS I am adding here a short copy paste of the code so people don't have to download the whole sample.

static void LoadPluginsInsideNewDomainUsingMarshalByRef()
{
    AppDomain appDomainPluginB = AppDomain.CreateDomain("appDomainPluginB");

    RemoteLoader loader = (RemoteLoader)appDomainPluginB.CreateInstanceAndUnwrap(
        "plugins", 
        "plugins.RemoteLoader");

    loader.LoadAndExecute("pluginB");

    //The app domain will be unloaded.
    //This is because pluginB has been loaded into appDomainPluginB but not
    //in AppDomain.CurrentDomain.
    //This means that AppDomain.CurrentDomain does not link/point to pluginB.
    AppDomain.Unload(appDomainPluginB);
}

public class RemoteLoader : MarshalByRefObject
{ 
    public void LoadAndExecute(string assemblyName)
    {
        Assembly pluginAassembly = AppDomain.CurrentDomain.Load(assemblyName);

        foreach (Type type in pluginAassembly.GetTypes())
        {
            if (type.GetInterface("IPlugin") != null)
            {
                object instance = Activator.CreateInstance(type, null, null);
                string s = ((IPlugin)instance).Execute("test");
                Console.WriteLine("Return from call is " + s);
            }
        }
    }
}

License

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

About the Author

Adrian Vintu

Austria Austria
Adrian is an old timer who has had the wonderful experience of working in a variety of quality environments and with various quality people.
 
Throughout time he has got down with Assembler, C++, Borland Delphi, Java, C#, Android etc.
He took part in projects concerning software security, industrial controllers, health-food and health-care ERPS, AI simulations, data mining and mobile development.
 
Adrian is always interested in the big picture of the things and wishes to become a great architect one day.
 
Adrian maintains a blog here http://adrianvintu.com[^] and is trying to make it as nice and helpful and user friendly as possible.

Comments and Discussions

 
GeneralMy vote of 5 Pinmembervendettamit23-Nov-12 20:40 
superbly explained with visuals.

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 | Mobile
Web02 | 2.8.140415.2 | Last Updated 15 Apr 2010
Article Copyright 2010 by Adrian Vintu
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid