Click here to Skip to main content
15,881,938 members
Articles / .NET

Unloadable Plugins

Rate me:
Please Sign up or sign in to vote.
5.00/5 (10 votes)
15 Apr 2010CPOL4 min read 19K   13   3
Creating a plugin architecture in .NET is pretty easy. Unless you need to unload the plugins - AppDomain.Unload just does not work.

Creating a plug in 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:

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

The domains should look like this:

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

C#
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 here.

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 plug in.

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:

C#
//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 plug in from the main domain, we execute the code in the second domain.

Here is how it looks like:

C#
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.

C#
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):

C#
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.

C#
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 information will actually be loaded into appDomainPluginB.

C#
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:

C#
//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 useful.

P.S.: Here, I am adding a short copy paste of the code so people don't have to download the whole sample.

C#
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)


Written By
Website Administrator none
Austria Austria
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionAbout link. Pin
Member 1039730531-Dec-18 11:57
Member 1039730531-Dec-18 11:57 
QuestionVery helpful Pin
MrMikeJJ6-Nov-14 1:35
MrMikeJJ6-Nov-14 1:35 
GeneralMy vote of 5 Pin
vendettamit23-Nov-12 20:40
professionalvendettamit23-Nov-12 20:40 

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.