在加载的appdomain中订阅事件

时间:2014-03-13 11:47:44

标签: c# events load .net-assembly appdomain

我在appdomains中处理事件有2个问题。我有我的主appdomain和一个新的appdomain加载一个插件,总共1个exe和2个dll。从主域调用加载的插件中的方法可以正常工作,但是我无法在插件中接收到主appdomain中生成的事件。我的事件接收方法是在新appdomain的上下文中(必须加载它我猜...我在这里很困惑)。我已根据此处提供的解决方案C# Dynamic Loading/Unloading of DLLs Redux (using AppDomain, of course)开展工作,因为我的最终计划是即时加载和卸载插件。这是迄今为止的代码:

这是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);
        }
    }
}

这是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();
        }
    }
}

这是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());
        }
    }
}

执行DynamicLoadTest.exe之前的文件放置是:

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

PluginBaseLib存在两次,因为DynamicLoadText.exe和SamplePlugin.dll都依赖它。

当我启动exe时,一切顺利,直到它击中" + ="此时我得到一个TargetInvocationException(InnerException:FileNotFoundException):

  

{"无法加载文件或程序集' DynamicLoadTest,版本= 1.0.0.0,   Culture = neutral,PublicKeyToken = null'或其中一个依赖项。该   系统找不到指定的文件。":" DynamicLoadTest,   Version = 1.0.0.0,Culture = neutral,PublicKeyToken = null"}

我尝试将SamplePlugin.dll移动到与DynamicLoadTest.exe相同的目录,并将AppDomainSetup.ApplicationBase更改为指向基目录并再次运行。这使得它全部运行而没有抛出异常,我甚至得到了事件:

  

[我的新应用域名]从SamplePlugin.MySamplePlugin收到了一个事件

我现在担心收到该事件的EventReceiver不是主应用程序域中的那个,而是在新appdomain(持有该插件的域)中运行的克隆。

那么,我该如何做到这一点,即让插件dll在Plugins目录中并在主appdomain的EventReceiver方法中接收事件?

编辑:

我试图更深入地了解这一点,但失败了。我可以在辅助appdomain中订阅事件的唯一方法是辅助appdomain加载主appdomain(然后存在两次)。我在主appdomain中使用静态分配的密钥检查了它,我在运行时更改了该密钥。只能从主应用程序域中看到此更改。键是从辅助应用程序域触发时从事件处理程序中看到的静态分配值。这显然不是我想要的。

我没有找到任何指导或解释(我能理解),解释我的尝试是否可行。

1 个答案:

答案 0 :(得分:3)

我找到了一个问题的解决方案,我的事件处理程序(Program.EventReceiver)将在辅助应用程序域中创建并在那里处理事件。问题是Program类不是从MarshalByRefObject继承而是似乎是可序列化的。我不希望Program继承MarshalByRefObject(实际上不知道是否允许)所以我将Program.EventReceiver移动到从MarshalByRefObject继承的自己的类中。我在Program中实例化了该类,现在EventReceiver正在接收主应用程序域中存在的事件。成功无处不在。

没有在Program上指定MarshalByRefObject,它被序列化(重复),这不是我想要的。

这就是我最终的结果。请注意我从来没有把它投入生产,我认为插件可能在垃圾收集过程中自动卸载或者不是由我的代码处理的对象超时。有一种解决方法,但我记不住了。

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);
    }
}

我使用这些构建后的命令:

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\"