在LoadFrom上下文中绑定到程序集时如何重现InvalidCastException

Mat*_*ith 3 c# serialization .net-assembly

Suzanne Cook的.NET CLR Notes中,她谈到了“ LoadFrom”上下文的危险。特别,

  • 如果Load上下文程序集尝试按显示名称加载此程序集,则默认情况下将找不到它(例如,当mscorlib.dll反序列化此程序集时)
  • 更糟糕的是,在探测路径上可能会找到具有相同标识但路径不同的程序集,从而导致稍后导致InvalidCastException,MissingMethodException或意外的方法行为。

如何在不显式加载程序集的两个不同版本的情况下通过反序列化来重现此行为?

Mat*_*ith 5

我创建了一个控制台应用程序A.exe,该应用程序通过类库B.dll间接加载(通过`Assembly.LoadFrom)并调用(通过反射)代码。

  • A.exe没有(不必要)对B.dll的引用,但是B.dll应该与A.exe在同一目录中
  • B.dll的副本应放在另一个目录中(这里我使用了名为LoadFrom的子目录),这是我们将使用的位置Assembly.LoadFrom

可执行文件

class Program
{
    static void Main(string[] args)
    {
        // I have a post build step that copies the B.dll to this sub directory.
        // but the B.dll also lives in the main directory alongside the exe:
        // mkdir LoadFrom
        // copy B.dll LoadFrom
        //
        var loadFromAssembly = Assembly.LoadFrom(@".\LoadFrom\B.dll");
        var mySerializableType = loadFromAssembly.GetType("B.MySerializable");

        object mySerializableObject = Activator.CreateInstance(mySerializableType);
        var copyMeBySerializationMethodInfo = mySerializableType.GetMethod("CopyMeBySerialization");

        try
        {
            copyMeBySerializationMethodInfo.Invoke(mySerializableObject, null);
        }
        catch (TargetInvocationException tie)
        {
            Console.WriteLine(tie.InnerException.ToString());
        }

        Console.ReadKey();
    }
}
Run Code Online (Sandbox Code Playgroud)

B.dll

namespace B
{
    [Serializable]
    public class MySerializable
    {
        public MySerializable CopyMeBySerialization()
        {
            return DeepClone(this);
        }

        private static T DeepClone<T>(T obj)
        {
            using (var ms = new MemoryStream())
            {
                var formatter = new BinaryFormatter();
                formatter.Serialize(ms, obj);
                ms.Position = 0;

                return (T)formatter.Deserialize(ms);
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

输出量

System.InvalidCastException: 
  [A]B.MySerializable cannot be cast to 
  [B]B.MySerializable. 
  Type A originates from 'B, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' 
    in the context 'Default' at location 'c:\Dev\bin\Debug\B.dll'. 
  Type B originates from 'B, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' 
    in the context 'LoadFrom' at location 'c:\Dev\bin\Debug\LoadFrom\B.dll'.

   at B.MySerializable.DeepClone[T](T obj)
   at B.MySerializable.CopyMeBySerialization()
Run Code Online (Sandbox Code Playgroud)

这是正在发生的事情:

  • 进行调用时formatter.Deserialize(ms),它将使用MemoryStream中存储的信息来确定需要创建哪种类型的对象(以及创建该对象所需的程序集)。
  • 它发现它需要B.dll并尝试加载它(从默认的“加载”上下文)。
  • 找不到当前加载的B.dll(因为它是在“ LoadFrom”上下文中加载的)。
  • 因此,尝试在通常的位置查找B.dll,该文件在ApplicationBase目录中找到并被加载。
  • 该B.dll中的所有类型都被认为与其他B.dll中的类型不同。因此,表达式中的强制转换(T)formatter.Deserialize(ms)失败。

附加条款:

  • 如果B.dll在A.exe可以使用找到它的某个地方不存在Assembly.Load,则将InvalidCastException出现SerializationException,提示消息“ 无法找到程序集'B,Version = 1.0.0.0,Culture = neutral,PublicKeyToken = null”。 '。
  • 即使使用已签名的程序集,也会出现相同的问题,但是使用已签名的程序集更令人震惊的是它可以加载已签名程序集的不同版本。也就是说,如果“ LoadFrom”上下文中的B.dll是1.0.0.0,但是在主目录中找到的B.dll是2.0.0.0,则序列化代码仍将加载错误的版本B.dll进行反序列化。
  • DeepClone我显示的代码似乎是在对象上进行深度克隆的较流行的方法之一。请参阅:在C#中深度克隆对象

因此,从任何已加载到“ LoadFrom”上下文中的代码中,您都无法成功使用反序列化(必须跳过附加箍以允许程序集在默认的“ Load”上下文中成功加载)。