9

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

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

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

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

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

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

4

3 回答 3

5

假设您正在通过Assembly.Load()or加载程序集Assembly.LoadFrom(),那么正如Chris ShainSerializationException 动态加载类型回答中所解释的那样,您可以使用该事件在反序列化期间加载动态程序集。但是,出于安全原因,您将希望防止加载完全意外的程序集。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;
                };
        }
    }
}

然后,在启动的某个地方,添加一个适当ResolveEventHandlerAppDomain.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>not的程序集的名称MyClass。有关如何执行此操作的详细信息,请参阅

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

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

顺便说一句,内置的基于 BinaryFormatter 的 .Net 序列化有哪些不足之处?对使用BinaryFormatter.

于 2019-12-10T21:19:24.143 回答
3

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

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

于 2013-09-18T20:54:22.440 回答
2

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

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

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

1.最简单的情况:只有汇编版本发生了变化

assemblyBinding您可以在 app.config 文件中添加一个简单的。只需将实际版本放入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


最后的想法:

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