使用AppDomain.Load()时发生的有趣错误

3

我正在尝试找到一种在运行时编译程序集并加载它们的方法。基本意图是将它们存储在数据库中而不是磁盘上。因此,我编写了一些代码,但看到了一个有趣的情况。这是我的代码:

//SumLib
namespace SumLib
{
    public class SumClass
    {
        public static int Sum(int a, int b)
        {
            return a + b;
        }
    }
}


// Console app
class Program
{

    public static void AssemblyLoadEvent(object sender, AssemblyLoadEventArgs args)
    {

        object[] tt = { 3, 6 };
        Type typ = args.LoadedAssembly.GetType("SumLib.SumClass");
        MethodInfo minfo = typ.GetMethod("Sum");
        int x = (int)minfo.Invoke(null, tt);
        Console.WriteLine(x);
    }

    static void Main(string[] args)
    {

        AppDomain apd = AppDomain.CreateDomain("newdomain", AppDomain.CurrentDomain.Evidence, AppDomain.CurrentDomain.SetupInformation);
        apd.AssemblyLoad += new AssemblyLoadEventHandler(AssemblyLoadEvent);

        FileStream fs = new FileStream("Sumlib.dll", FileMode.Open);
        byte[] asbyte = new byte[fs.Length];
        fs.Read(asbyte, 0, asbyte.Length);
        fs.Close();
        fs.Dispose();

//      File.Delete("Sumlib.dll");

        apd.Load(asbyte);

        Console.ReadLine();
    }
}

当删除行已注释时,代码可以完美运行。如果取消注释,则应用程序域将加载程序集,AssemblyLoadEvent() 方法运行,控制台上出现数字9,但是当方法结束时,apd.Load() 抛出一个错误:"无法加载文件或程序集",这很合理。

问题是:没有磁盘上的程序集文件,如何让 AssemblyLoadEvent() 方法运行?

如果该方法以某种方式通过原始二进制数据运行,则是否有任何方法使 appdomain 成功完成 Load() 方法?


你正在尝试从一个 byte[] 中加载程序集。是这样吗? - Sam B
3个回答

6
它成功地将程序集加载到“newdomain”中,并在“newdomain”中调用事件处理程序(如果在事件处理程序中打印当前域,则可以验证此操作)。最后,它创建要传回的返回值。尽管在您的示例代码中忽略了该返回值,但它仍然被创建。异常发生在跨域编组期间,因为反序列化还想将程序集加载到默认域中。
以下是来自Mono的异常调用堆栈:
  at System.AppDomain.Load (System.String assemblyString, System.Security.Policy.Evidence assemblySecurity, Boolean refonly) [0x00000] in <filename unknown>:0
  at System.AppDomain.Load (System.String assemblyString) [0x00000] in <filename unknown>:0
  at (wrapper remoting-invoke-with-check) System.AppDomain:Load (string)
  at System.Reflection.Assembly.Load (System.String assemblyString) [0x00000] in <filename unknown>:0
  at System.UnitySerializationHolder.GetRealObject (StreamingContext context) [0x00000] in <filename unknown>:0
  at System.Runtime.Serialization.ObjectRecord.LoadData (System.Runtime.Serialization.ObjectManager manager, ISurrogateSelector selector, StreamingContext context) [0x00000] in <filename unknown>:0
  at System.Runtime.Serialization.ObjectManager.DoFixups () [0x00000] in <filename unknown>:0
  at System.Runtime.Serialization.Formatters.Binary.ObjectReader.ReadNextObject (System.IO.BinaryReader reader) [0x00000] in <filename unknown>:0
  at System.Runtime.Serialization.Formatters.Binary.ObjectReader.ReadObjectGraph (BinaryElement elem, System.IO.BinaryReader reader, Boolean readHeaders, System.Object& result, System.Runtime.Remoting.Messaging.Header[]& headers) [0x00000] in <filename unknown>:0
  at System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.NoCheckDeserialize (System.IO.Stream serializationStream, System.Runtime.Remoting.Messaging.HeaderHandler handler) [0x00000] in <filename unknown>:0
  at System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize (System.IO.Stream serializationStream) [0x00000] in <filename unknown>:0
  at System.Runtime.Remoting.RemotingServices.DeserializeCallData (System.Byte[] array) [0x00000] in <filename unknown>:0
  at (wrapper xdomain-invoke) System.AppDomain:Load (byte[])
  at (wrapper remoting-invoke-with-check) System.AppDomain:Load (byte[])
  at Program.Main (System.String[] args) [0x00000] in <filename unknown>:0

编辑:这里有来自MSDN的确认:

尝试在不是当前应用程序域的目标应用程序域上调用Load将导致成功加载目标应用程序域中的程序集。由于Assembly不是MarshalByRefObject,因此当该方法尝试返回已加载程序集的程序集到当前应用程序域时,公共语言运行时将尝试将程序集加载到当前应用程序域中,从而可能导致加载失败。加载到当前应用程序域中的程序集可能与首先加载的程序集不同,如果两个应用程序域的路径设置不同。


3

所以你试图从一个 byte[] 中加载一个程序集并调用它的方法。我不建议你使用这种方式(即使用 AssemblyLoad 事件进行操作),因为它会针对每个依赖项都被调用。

@Jester 关于使用 Load() 方法从父域加载程序集是正确的。为了纠正这个问题,我建议使用像这样的包装类:

// Console app 
class Program 
{  
    public class AssemblyLoader : MarshalByRefObject
    {
        public void LoadAndCall(byte[] binary)
        {
            Assembly loadedAssembly = AppDomain.CurrentDomain.Load(binary);
            object[] tt = { 3, 6 };
            Type typ = loadedAssembly.GetType("SumLib.SumClass");
            MethodInfo minfo = typ.GetMethod("Sum", BindingFlags.Static | BindingFlags.Public);
            int x = (int)minfo.Invoke(null, tt);
            Console.WriteLine(x);
        }
    }

    static void Main()
    {
        AppDomain apd = AppDomain.CreateDomain("newdomain", AppDomain.CurrentDomain.Evidence, AppDomain.CurrentDomain.SetupInformation);
        FileStream fs = new FileStream("Sumlib.dll", FileMode.Open);
        byte[] asbyte = new byte[fs.Length];
        fs.Read(asbyte, 0, asbyte.Length);
        fs.Close();
        fs.Dispose();
        File.Delete("Sumlib.dll");    

        AssemblyLoader loader = (AssemblyLoader)apd.CreateInstanceAndUnwrap(typeof(AssemblyLoader).Assembly.FullName, typeof(AssemblyLoader).FullName);
        loader.LoadAndCall(asbyte);
        Console.ReadLine();
      }
}  

嗯,很有意思。因此,卸载应用程序域也会将程序集从内存中移除,对吗? - ali_bahoo
@sad_man:由于您正在通过反射调用目标程序集,我建议在LoadAndCall内部放置try-catch块以检测反射问题。 - Sam B
什么类型的问题?你能给我指一些阅读材料吗? - ali_bahoo
@sad_man:反射本身就是一个大主题。更改目标程序集可能会导致解析“Sum”方法时出现问题(例如:添加另一个采用不同参数的Sum方法。在这种情况下,您将需要通过其参数来识别该方法)。您可以查看MSDN(http://msdn.microsoft.com/en-us/library/f7ykdhsy.aspx)。 - Sam B

0

为什么不使用影子复制参数?它可能会对你有所帮助。


主要是因为它需要将被影子复制的程序集存储在应用程序目录或其子目录中,由ApplicationBase指定。这样我甚至可以将程序集存储在数据库中,并在必要时加载它们。 - ali_bahoo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接