Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# Dynamic Loading/Unloading of DLLs Redux (using AppDomain, of course)

I've read as many different version of this question as are on Stack Overflow, as well as every blue link on the front page of 3 different Google searches for tutorials, as well as the MSDN (which is kind of shallow beyond executing assemblies). I can only think of my efforts to get Tao to work as a good test case, but believe me, I've tried with a simple string return, a double, a function with parameters, too. Whatever my problem is, it isn't Tao.

Basically I want to create a testLibraryDomain.CreateInstance() of my Draw class in the GLPlugin namespace.

        if( usePlugin )
        {
                AppDomain testLibraryDomain = AppDomain.CreateDomain( "TestGLDomain2" );

                //What the heck goes here so that I can simply call
                //the default constructor and maybe a function or two?

                AppDomain.Unload( testLibraryDomain );
        }
        Gl.glBegin( Gl.GL_TRIANGLES );

I know for a fact that:

namespace GLPlugin
{
    public class DrawingControl : MarshalByRefObject
    {
        public DrawingControl()
        {
            Gl.glColor3f( 1.0f , 0.0f , 0.0f );

            //this is a test to make sure it passes
            //to the GL Rendering context... success
        }
    }
}

indeed changes the pen color. It works when I give it a static void Main( string args[] ) entry point and I call testLibraryDomain.ExecuteAssembly( thePluginFilePath ) Whether or not a direct ExecuteAssembly would work had concerned me, as I was not certain the GL Calls would make it into the "top level" AppDomain's OpenGL context. It even lets me overwrite the assembly and change the Pen Color a second time. Unfortunately giving it an executable entry point means that a popup console interrupts me then goes away. It also works when I simply give it a reference in the Project and create a regular GLPlugin.DrawingTool tool = new GLPlugin.DrawingControl(), or even creating a someAssembly = Assembly.LoadFrom( thePluginFilePath ) (which of course and unfortunately, locks the assembly, preventing replacement/recompilation).

When using any of the various methods I've tried, I always get "the given assembly name or its code base is invalid." I promise, it is valid. Something in the way I'm trying to load it is not.

One thing I know I'm lacking is a correct setup for the testLibraryDomain.CreateInstance( string assemblyName , string typeName);

As far as I can tell, the assemblyName argument is not the filepath to the assembly file. Is it the namespace, or even just the assembly name, ie: GLPlugin? If so, where do I reference the actual file? There is no someAppDomain.LoadFrom( someFilename ), though it would be dang handy if there were. Additionally, what the heck is the Type, and string typeName at that? I don't want to put in "Object" here do I, since is not creating type other than an instance of an object? I've also tried CreateInstanceAndUnwrap( ... , ... ) with the same lack of a fundamental understanding of AppDomain. Usually I can muddle through tutorials and get things to work, even though I often don't understand the "Why?"... not so here. Usually it is helpful for me to look up six different tutorials... not so here again, but because every one takes a fundamentally (or what appears to be so) approach.

So please ELI5... I want to load an instance of a class from a dll in a separate AppDomain, maybe run a few functions, and unload it. Eventually create a list of these functions as List, removing/updating as necessary... I'd love to be able to pass arguments to them as well, but that will be step 2. According to StackOverflow, I have to learn about serializable which I will put off for another day. (I imagine you'll be able to figure from my example what I'm trying to do.)

like image 522
Adam Avatar asked Nov 19 '12 21:11

Adam


1 Answers

Ok, we have to clarify few things. First, if you want to be able to load and unload dlls to different AppDomain without locking the file iteslf, maybe you can use approach like this:

AppDomain apd = AppDomain.CreateDomain("newdomain");
using(var fs = new FileStream("myDll.dll", FileMode.Open))
{
    var bytes = new byte[fs.Length];
    fs.Read(bytes, 0, bytes .Length);
    Assembly loadedAssembly = apd.Load(bytes);
} 

This way, you won't be locking the file, and you should be able to later, unload domain, recompile the file and load it with newer version later. But i'm not 100% sure if this won't break your application.

And that's because of second thing. If you will be using CreateInstanceAndUnwrap method, according to MSDN, you have to load the assembly in both appdomains - the one that is calling, and the one from which you are calling. And this may end in a situation, when you have two different dlls loaded in AppDomains.

The assembly that contains unwrapped class must be loaded into both application domains, but it can load other assemblies that exist only in the new application domain.

I don't remember right now, but i think behavior of object creation in both app domains will be different when you will call CreateInstanceAndUnwrap, but i don't remember the details.

For your plugin architecture, you may want to read this blog post. About how to handle Dynamic Plugins using the AppDomain Class to Load and Unload Code

EDIT

I forgot how this AppDomains works and i might introduce some confusion. I prepared short example how 'plugin' architecture might work. It's quite similar to what was described in blog i put earlier, and here is my sample which uses Shadow Copying. If for some reasons you don't want to use it, it can be quite easly changed to use AppDomain.Load(byte[] bytes)

We have 3 assemblies, first one is base plugin assembly, which will work as a proxy, and will be loaded in all AppDomains (in our case - in main app domain and in plugin app domain).

namespace PluginBaseLib
{
    //Base class for plugins. It has to be delivered from MarshalByRefObject,
    //cause we will want to get it's proxy in our main domain. 
    public abstract class MyPluginBase : MarshalByRefObject 
    {
        protected MyPluginBase ()
        { }

        public abstract void DrawingControl();
    }

    //Helper class which instance will exist in destination AppDomain, and which 
    //TransparentProxy object will be used in home AppDomain
    public class MyPluginFactory : MarshalByRefObject
    {
        //This method will be executed in destination AppDomain and proxy object
        //will be returned to home AppDomain.
        public MyPluginBase CreatePlugin(string assembly, string typeName)
        {
            Console.WriteLine("Current domain: {0}", AppDomain.CurrentDomain.FriendlyName);
            return (MyPluginBase) Activator.CreateInstance(assembly, typeName).Unwrap();
        }
    }

    //Small helper class which will show how to call method in another AppDomain. 
    //But it can be easly deleted. 
    public class MyPluginsHelper
    {
        public static void LoadMyPlugins()
        {
            Console.WriteLine("----------------------");
            Console.WriteLine("Loading plugins in following app domain: {0}", AppDomain.CurrentDomain.FriendlyName);
            AppDomain.CurrentDomain.Load("SamplePlugin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null");
            Console.WriteLine("----------------------");
        }
    }
}

Here we will have another assembly with our dummy plugin, called SamplePlugin.dll and stored under "Plugins" folder. It has PluginBaseLib.dll referenced

namespace SamplePlugin
{
    public class MySamplePlugin : MyPluginBase
    {
        public MySamplePlugin()
        { }

        public override void DrawingControl()
        {
            var color = Console.ForegroundColor;
            Console.ForegroundColor = ConsoleColor.Green;
            Console.WriteLine("----------------------");
            Console.WriteLine("This was called from app domian {0}", AppDomain.CurrentDomain.FriendlyName );
            Console.WriteLine("I have following assamblies loaded:");
            foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
            {
                Console.WriteLine("\t{0}", assembly.GetName().Name);
            }
            Console.WriteLine("----------------------");
            Console.ForegroundColor = color;
        }
    }
}

And last assembly (simple console app) which will reference only PluginBaseLib.dll and

namespace ConsoleApplication1
{
    //'Default implementation' which doesn't use any plugins. In this sample 
    //it just lists the assemblies loaded in AppDomain and AppDomain name itself.
    public static void DrawControlsDefault()
    {
        Console.WriteLine("----------------------");
        Console.WriteLine("No custom plugin, default app domain {0}", AppDomain.CurrentDomain.FriendlyName);
        Console.WriteLine("I have following assamblies loaded:");
        foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
        {
            Console.WriteLine("\t{0}", assembly.GetName().Name);
        }
        Console.WriteLine("----------------------");
    }

    class Program
    {
        static void Main(string[] args)
        {
            //Showing that we don't have any additional plugins loaded in app domain. 
            DrawControlsDefault();

            var appDir = AppDomain.CurrentDomain.BaseDirectory;
            //We have to create AppDomain setup for shadow copying 
            var appDomainSetup = new AppDomainSetup
                                 {
                                     ApplicationName = "", //with MSDN: If the ApplicationName property is not set, the CachePath property is ignored and the download cache is used. No exception is thrown.
                                     ShadowCopyFiles = "true",//Enabling ShadowCopy - yes, it's string value
                                     ApplicationBase = Path.Combine(appDir,"Plugins"),//Base path for new app domain - our plugins folder
                                     CachePath = "VSSCache"//Path, where we want to have our copied dlls store. 
                                 };
        var apd = AppDomain.CreateDomain("My new app domain", null, appDomainSetup);

        //Loading dlls in new appdomain - when using shadow copying it can be skipped,
        //in CreatePlugin method all required assemblies will be loaded internaly,  
        //Im using this just to show how method can be called in another app domain. 
        //but it has it limits - method cannot return any values and take any parameters.

        //apd.DoCallBack(new CrossAppDomainDelegate(MyPluginsHelper.LoadMyPlugins));

        //We are creating our plugin proxy/factory which will exist in another app domain 
        //and will create for us objects and return their remote 'copies'. 
        var proxy = (MyPluginFactory) apd.CreateInstance("PluginBaseLib", "PluginBaseLib.MyPluginFactory").Unwrap();

        //if we would use here method (MyPluginBase) apd.CreateInstance("SamplePlugin", "SamplePlugin.MySamplePlugin").Unwrap();
        //we would have to load "SamplePlugin.dll" into our app domain. We may not want that, to not waste memory for example
        //with loading endless number of types.
        var instance = proxy.CreatePlugin("SamplePlugin", "SamplePlugin.MySamplePlugin");
        instance.DrawingControl();

        Console.WriteLine("Now we can recompile our SamplePlugin dll, replace it in Plugin directory and load in another AppDomain. Click Enter when you ready");
        Console.ReadKey();

        var apd2 = AppDomain.CreateDomain("My second domain", null, appDomainSetup);
        var proxy2 = (MyPluginFactory)apd2.CreateInstance("PluginBaseLib", "PluginBaseLib.MyPluginFactory").Unwrap();
        var instance2 = proxy2.CreatePlugin("SamplePlugin", "SamplePlugin.MySamplePlugin");
        instance2.DrawingControl();

        //Now we want to prove, that this additional assembly was not loaded to prmiary app domain. 
        DrawControlsDefault();

        //And that we still have the old assembly loaded in previous AppDomain.
        instance.DrawingControl();

        //App domain is unloaded so, we will get exception if we try to call any of this object method.
        AppDomain.Unload(apd);
        try
        {
            instance.DrawingControl();
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
        }

        Console.ReadKey();
    }
}

}

Shadow copying seems to be very convenient.

like image 108
Marek Kembrowski Avatar answered Sep 22 '22 10:09

Marek Kembrowski