1

I have 2 problems handling events across appdomains. I have my main appdomain and a new appdomain that loads a plugin, in total 1 exe and 2 dll's. Calling methods in the loaded plugin from the main domain works, but I am not able to receive an event generated in the plugin into the main appdomain. My event receiving method is however being in the context of the new appdomain (that must have loaded it I guess... I am confused here). I have worked based on the solution provided here C# Dynamic Loading/Unloading of DLLs Redux (using AppDomain, of course) as my final program is to be loading and unloading plugins on the fly. Here is the code so far:

This is DynamicLoadText.exe:

namespace DynamicLoadTest
{
    class Program
    {
        static void EventReceiver(object sender, EventArgs e)
        {
            Console.WriteLine("[{1}] Received an event from {0}", sender.ToString(), AppDomain.CurrentDomain.FriendlyName);
        }

        static void Main(string[] args)
        {
            var appDir = AppDomain.CurrentDomain.BaseDirectory;

            var appDomainSetup = new AppDomainSetup
                                 {
                                     ApplicationName = "",
                                     ShadowCopyFiles = "true",
                                     ApplicationBase = Path.Combine(appDir, "Plugins"),
                                     CachePath = "VSSCache"
                                 };

            var apd = AppDomain.CreateDomain("My new app domain", null, appDomainSetup);
            var proxy = (MyPluginFactory)apd.CreateInstance("PluginBaseLib", "PluginBaseLib.MyPluginFactory").Unwrap();
            var instance = proxy.CreatePlugin("SamplePlugin", "SamplePlugin.MySamplePlugin");

            instance.MyEvent += EventReceiver;
            instance.TestEvent();
            instance.MyEvent -= EventReceiver;

            AppDomain.Unload(apd);
        }
    }
}

This is PluginBaseLib.dll:

namespace PluginBaseLib
{
    public abstract class MyPluginBase : MarshalByRefObject
    {
        public abstract event EventHandler MyEvent;

        protected MyPluginBase()
        { }

        public abstract void TestEvent();
    }

    public class MyPluginFactory : MarshalByRefObject
    {
        public MyPluginBase CreatePlugin(string assemblyName, string typeName)
        {
            return (MyPluginBase)Activator.CreateInstance(assemblyName, typeName).Unwrap();
        }
    }
}

And this is SamplePlugin.dll:

namespace SamplePlugin
{
    public class MySamplePlugin : MyPluginBase
    {
        public override event EventHandler MyEvent;

        public MySamplePlugin()
        { }

        public override void TestEvent()
        {
            if (MyEvent != null)
                MyEvent(this, new EventArgs());
        }
    }
}

The file-placement prior to execution of the DynamicLoadTest.exe is this:

dir\DynamicLoadText.exe
dir\PluginBaseLib.dll
dir\Plugins\PluginBaseLib.dll
dir\Plugins\SamplePlugin.dll

PluginBaseLib is present twice as both DynamicLoadText.exe and SamplePlugin.dll depend on it.

When I start the exe, all goes well until it hits the "+=" at which point I get a TargetInvocationException (InnerException: FileNotFoundException):

{"Could not load file or assembly 'DynamicLoadTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified.":"DynamicLoadTest, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"}

I tried to move SamplePlugin.dll to the same directory as DynamicLoadTest.exe and changed the AppDomainSetup.ApplicationBase to point at the base directory and run again. This made it all run without throwing exceptions and I even got en event:

[My new app domain] Received an event from SamplePlugin.MySamplePlugin

I am now concerned that my EventReceiver that received the event isn't the one in the main appdomain, but is a clone running in the new appdomain (the one that holds the plugin).

So, how to I make this just right, i.e. having the plugin dll's be in the Plugins directory and receiving the event in the main appdomain's EventReceiver method?

Edit:

I have tried to get more insight into this but failed. The only way I can subscribe to the event in the secondary appdomain is when the secondary appdomain loads the primary appdomain (which then exists twice). I have checked it using a statically assigned key in the primary appdomain which I change during runtime. The change is only seen from the primary appdomain. The key is the statically assigned value when seen from the event handler when triggered by the secondary appdomain. This is clearly not what I want.

I have found no guide or explanation (I could understand) explaining if what I try is possible or not.

Community
  • 1
  • 1
galmok
  • 869
  • 10
  • 21
  • Please read my blog on cross-AppDomain Communication https://blog.vcillusion.co.in/sending-events-through-application-domain-boundary/ – vCillusion Jun 02 '18 at 22:56

1 Answers1

3

I found a solution to the issue where my event handler (Program.EventReceiver) would be created in the secondary appdomain and handled events there. The issue is that the Program class isn't inheriting from MarshalByRefObject but seems to be serializable. I did not want Program to inherit MarshalByRefObject (actually don't know if it is allowed) so I moved Program.EventReceiver into its own class that inherited from MarshalByRefObject. I instantiated that class in Program and now EventReceiver is receiving events while existing in the primary appdomain. Success all around.

Without specifying MarshalByRefObject on Program, it was being serialized (duplicated) and this was not what I wanted.

This is what I ended up with. Please note I never got to put it into production and I think that the plugin may be unloaded automatically during garbage collection or object timeout handled not by my code. There is a workaround, but I cannot remember it.

DynamicLoadTest:

class Program
{
    public static string key = "I am in the WRONG assembly.";

    static void Main(string[] args)
    {
        key = "I am in the primary assembly";
        plap pop = new plap();

        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 = appDir,//Base path for new app domain - our plugins folder
                                 CachePath = "VSSCache",//Path, where we want to have our copied dlls store.                                      
                             };
        Console.WriteLine("Looking for plugins in {0}\\Plugins", appDir);
        var apd = AppDomain.CreateDomain("My new app domain", null, appDir, "Plugins", true);

        //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();

        var instance = proxy.CreatePlugin("SamplePlugin", "SamplePlugin.MySamplePlugin");

        Console.WriteLine("[appdomain:{0}] Main: subscribing for event from remote appdomain.", AppDomain.CurrentDomain.FriendlyName);
        instance.MyEvent += pop.EventReceiver;

        instance.TestEvent();

        Console.WriteLine("[appdomain:{0}] Main: Waiting for event (press return to continue).", AppDomain.CurrentDomain.FriendlyName);
        Console.ReadKey();
        instance.MyEvent -= pop.EventReceiver;

        Console.WriteLine("[appdomain:{0}] Main: Unloading appdomain: {1}", AppDomain.CurrentDomain.FriendlyName, apd.FriendlyName);
        AppDomain.Unload(apd);
        Console.ReadKey();
    }
}

class plap : MarshalByRefObject
{
    public plap() { }

    public void EventReceiver(object sender, EventArgs e)
    {
        Console.WriteLine("[appdomain:{1}] Received an event from {0} [key: {2}]", sender.ToString(), AppDomain.CurrentDomain.FriendlyName, Program.key);
    }
}

MyPluginBase:

//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
{
    public abstract event EventHandler MyEvent;

    protected MyPluginBase()
    { }

    public abstract void TestEvent();
}

//Helper class which instance will exist in destination AppDomain, and which 
//TransparentProxy object will be used in home AppDomain
public class MyPluginFactory : MarshalByRefObject
{
    //public event EventHandler MyEvent;

    //This method will be executed in destination AppDomain and proxy object
    //will be returned to home AppDomain.
    public MyPluginBase CreatePlugin(string assemblyName, string typeName)
    {
        Console.WriteLine("[appdomain:{0}] CreatePlugin {1} {2}", AppDomain.CurrentDomain.FriendlyName, assemblyName, typeName);
        return (MyPluginBase)Activator.CreateInstance(assemblyName, typeName).Unwrap();
    }
}

MySamplePlugin:

public class MySamplePlugin : MyPluginBase
{
    public override event EventHandler MyEvent;

    public MySamplePlugin()
    { }

    public override void TestEvent()
    {
        Console.WriteLine("[appdomain:{0}] TestEvent: setting up delayed event.", AppDomain.CurrentDomain.FriendlyName);
        System.Threading.Timer timer = new System.Threading.Timer((x) => {
            Console.WriteLine("[appdomain:{0}] TestEvent: firing delayed event.", AppDomain.CurrentDomain.FriendlyName);
            if (MyEvent != null)
                MyEvent(this, new EventArgs());            
        }, null, 1000, System.Threading.Timeout.Infinite);
    }
}

I use these post-build commands:

DynamicLoadTest:

mkdir "$(TargetDir)Plugins" || cmd /c "exit /b 0"
mkdir "$(TargetDir)VSSCache" || cmd /c "exit /b 0"

SamplePlugin:

xcopy /i /e /s /y /f "$(TargetPath)" "$(SolutionDir)DynamicLoadTest\bin\$(ConfigurationName)\Plugins\"
xcopy /i /e /s /y /f "$(TargetDir)PluginBaseLib.dll" "$(SolutionDir)DynamicLoadTest\bin\$(ConfigurationName)\Plugins\"
galmok
  • 869
  • 10
  • 21