【问题标题】:Binary serialization with dynamically loaded .Net assembly使用动态加载的 .Net 程序集进行二进制序列化
【发布时间】:2013-09-18 20:22:00
【问题描述】:

我将我的类的一个实例序列化到一个文件中(使用BinaryFormatter

之后,在另一个项目中,我想反序列化这个文件,但它不起作用,因为我的新项目没有旧类的描述。 .Deserialize() 出现异常

Unable to find assembly '*MyAssembly, Version=1.9.0.0, Culture=neutral, PublicKeyToken=null'.*".

但我有包含我想要反序列化的旧类的描述的程序集的 .DLL。

我不想在项目中添加对这个 DLL 的引用(我希望能够反序列化任何类型的程序集的类...)

如何通知序列化器/反序列化器使用我动态加载的程序集?

【问题讨论】:

标签: c# .net serialization .net-assembly


【解决方案1】:

假设您通过Assembly.Load()Assembly.LoadFrom() 加载程序集,然后如this answerSerializationException for dynamically loaded TypeChris Shain 所述,您可以使用AppDomain.AssemblyResolve 事件在反序列化期间加载动态程序集。但是,出于安全原因,您需要防止加载完全意外的程序集。

一种可能的实现方式是引入以下内容:

public class AssemblyResolver
{
    readonly string assemblyFullPath;
    readonly AssemblyName assemblyName;

    public AssemblyResolver(string assemblyName, string assemblyFullPath)
    {
        // You might want to validate here that assemblyPath really is an absolute not relative path.
        // See e.g. https://stackoverflow.com/questions/5565029/check-if-full-path-given
        this.assemblyFullPath = assemblyFullPath;
        this.assemblyName = new AssemblyName(assemblyName);
    }

    public ResolveEventHandler AssemblyResolve
    {
        get
        {
            return (o, a) =>
                {
                    var name = new AssemblyName(a.Name);
                    if (name.Name == assemblyName.Name) // Check only the name if you want to ignore version.  Otherwise you can just check string equality.
                        return Assembly.LoadFrom(assemblyFullPath);
                    return null;
                };
        }
    }
}

然后,在启动的某个地方,将适当的ResolveEventHandler 添加到AppDomain.CurrentDomain.AssemblyResolve,例如如下:

class Program
{
    const string assemblyFullPath = @"C:\Full-path-to-my-assembly\MyAssembly.dll";
    const string assemblyName = @"MyAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null";

    static Program()
    {
        AppDomain.CurrentDomain.AssemblyResolve += new AssemblyResolver(assemblyName, assemblyFullPath).AssemblyResolve;
    }

ResolveEventHandler 检查请求的程序集是否具有您的动态程序集的名称,如果有,则从预期的完整路径加载当前版本。

另一种方法是编写自定义SerializationBinder 并将其附加到BinaryFormatter.Binder。在BindToType (string assemblyName, string typeName) 中,活页夹需要检查属于您的动态程序集的类型,并适当地绑定到它们。这里的技巧是处理动态加载的类型嵌套在另一个程序集的泛型中的情况,例如一个List<MyClass>。在这种情况下,assemblyName 将是 List<T> 而不是 MyClass 的程序集的名称。有关如何执行此操作的详细信息,请参阅

comments @sgnsajgon 问中,我想知道为什么我不能像在项目中明确引用已签名的程序集时那样反序列化流 - 只是 formatter.Deserialize(stream) 而没有别的。

虽然我不知道微软员工在设计这些课程时的想法(回到.Net 1.1),但可能是因为:

顺便说一句,What are the deficiencies of the built-in BinaryFormatter based .Net serialization? 对您在使用BinaryFormatter 时可能遇到的其他问题进行了有用的概述。

【讨论】:

    【解决方案2】:

    二进制序列化对 DLL Hell 持严肃态度。它记录了数据序列化时包含该类型的确切程序集。 坚持在反序列化数据时找到准确的程序集。确保序列化数据与类型匹配的唯一方法,采取任何捷径只会确保您在幸运时获得异常,在您不幸运时获得垃圾数据。这种情况迟早会发生的可能性是 100%。

    因此,您需要完全放弃可以使用“动态加载的程序集”并让它“反序列化任何类型的类”的想法,这是一种错觉。您可以旋转命运之轮并在 app.exe.config 文件中放置 <bindingRedirect> 以强制 CLR 使用不同的程序集版本。处理事故现在是你的责任。许多程序员抓住了机会,很少有人从经验中回来而不吸取新的教训。必须这样做才能意识到后果。所以继续吧。

    【讨论】:

    • 我检查用于序列化实例的程序集是否与动态加载到反序列化的程序集相同。这些程序集似乎相同:相同的名称/版本/区域性/GUID。当我浏览这些 Reflection.Assemly 时,发现差异: -locations -filename (renamed dll) -evidences -# of class 这些程序集并不完全相同(我知道这一点),但具有相同的序列化标识和描述类: - 类的数量应该不是问题,因为只有一个类标记为 [Serializable] - 这个类具有相同的可序列化字段描述。
    • .Net 认为这些程序集不同的标准是什么?
    • 你说And insists to find that exact assembly back when it deserializes the data。即使是签名的程序集,我也有例外。在动态汇编的情况下不能以任何方式反序列化。在引用程序集的情况下没有问题。
    • @dbc 是的,有一些解决方法,但我想知道为什么我不能像在项目中显式引用签名程序集时那样反序列化流 - 只是 formatter.Deserialize(stream) 而没有别的。
    【解决方案3】:

    首先,关于二进制序列化的一些事实(如果您只对解决方案感兴趣,请跳过它们):

    • 二进制序列化的目标是制作对象的“按位”副本。这通常涉及私有字段的序列化,这可能会因版本而异。如果反序列化始终与序列化在同一进程中发生(典型用例:深度克隆、撤消/重做等),这不是问题。
    • 因此,如果反序列化可能发生在不同的环境中(包括不同的平台、框架版本、不同版本的程序集,甚至是同一程序集的混淆版本),则不建议使用二进制序列化。如果您知道其中任何一个都适用于您的情况,那么请考虑使用公共成员的基于文本的序列化,例如 XML 或 JSON 序列化。
    • 好像是微软started to abandonBinaryFormatter。虽然它只会在 .NET 5 中被删除/标记为过时(不过可以用作包),但 .NET Core 2/3 中也有许多类型,它们曾经在 .NET Framework 中可序列化但在 .NET Core 中不再可序列化(例如,TypeEncodingMemoryStreamResourceSet、委托等)。

    如果您仍然确定要使用 BinaryFormatter 解决问题,您有以下选择:

    1.最简单的情况:只有程序集版本发生了变化

    您可以在 app.config 文件中添加一个简单的assemblyBinding。只需将实际版本放在newVersion 属性中即可。

    <runtime>
      <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
        <dependentAssembly>
          <assemblyIdentity name="MyAssembly" publicKeyToken="null" culture="neutral" />
          <bindingRedirect oldVersion="0.0.0.0-2.0.0.0" newVersion="2.0.0.0" />
        </dependentAssembly>
      </assemblyBinding>
    </runtime>
    

    2。程序集名称和/或类型名称也已更改(或者如果您更喜欢编程解决方案)

    IFormatter 实现(因此也有BinaryFormatter)具有Binder 属性。您可以使用它来控制程序集/类型名称解析:

    internal class MyBinder : SerializationBinder
    {
        public override Type BindToType(string assemblyName, string typeName)
        {
            // mapping the known old type to the new one
            if (assemblyName.StartsWith("MyAssembly, ") && typeName == "MyNamespace.MyOldType")
                return typeof(MyNewType);
    
            // for any other type returning null to apply the default resolving logic:
            return null;
        }
    }
    

    用法:

    var formatter = new BinaryFormatter { Binder = new MyBinder() };
    return (MyNewType)formatter.Deserialize(myStream);
    

    如果您只需要对程序集版本不敏感的解析器,您可以使用WeakAssemblySerializationBinder

    3.新类型的内部结构也发生了变化

    由于 OP 没有涵盖这个案例,我不会太深入细节。 TL;DR: 在这种情况下,您需要设置IFormatter.SurrogateSelector 属性。如果类型名称和内部布局都已更改,则可以将其与 Binder 属性一起使用。如果您有兴趣,CustomSerializerSurrogateSelector 类的 Remarks 部分有一些可能的子案例。


    最后的想法:

    • 问题中的错误消息暗示使用BinaryFormatter 可能不是您实现目标的最佳选择。仅当您确定要使用二进制序列化时才使用上述解决方案。否则,您可以尝试改用 XML 或 JSON 序列化,它们基本上由公共成员对类型进行序列化,并且不存储任何程序集信息。
    • 如果您想使用我上面链接的活页夹/代理选择器,您可以从NuGet 下载库。它实际上还包含一个替代binary serializer(免责声明:由我撰写)。虽然它本身支持许多简单类型和集合(因此没有程序集标识存储在序列化流中)用于自定义类型,但您可能会遇到问题中出现的相同问题。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-08-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-08-16
      • 1970-01-01
      • 2023-03-20
      • 1970-01-01
      相关资源
      最近更新 更多