具有动态加载的.Net程序集的二进制序列化

时间:2013-09-18 20:22:00

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

我将类的一个实例序列化为一个文件(BinaryFormatter

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

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

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

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

如何通知Serializer / Deserializer使用我动态加载的程序集?

3 个答案:

答案 0 :(得分:4)

假设您正在通过Assembly.Load()Assembly.LoadFrom()加载程序集,然后按照this answerSerializationException for dynamically loaded Type Chris 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中问,我想知道为什么不能像在项目中显式引用签名程序集时那样,对流进行反序列化吗?

尽管我不知道Microsoft员工在设计这些类时的想法(返回.Net 1.1),但这可能是因为:

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

答案 1 :(得分:2)

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

  • 二进制序列化的目标是对对象进行“按位”复制。这通常涉及私有字段的序列化,这可能会因版本而异。如果反序列化总是在与序列化相同的过程中发生,则这不是问题(典型用例:深度克隆,撤消/重做等)。
  • 因此,如果反序列化可以在不同的环境(包括不同的平台,框架版本,程序集的不同版本,甚至同一程序集的混淆版本)中进行,则不建议使用二进制序列化。如果您知道其中任何一种都适合您的情况,请考虑使用公共成员基于文本的序列化,例如XML或JSON序列化。
  • 似乎是Microsoft started to abandon BinaryFormatter。尽管仅在.NET 5中将其删除/标记为过时(不过,可以将其用作程序包),但.NET Core 2/3中也有很多类型,这些类型曾经在.NET Framework中可序列化。但无法在.NET Core中进行序列化(例如TypeEncodingMemoryStreamResourceSet,委托等)。

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

1。最简单的情况:仅程序集版本已更改

您可以将简单的assemblyBinding添加到a​​pp.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下载库。它实际上还包含一个替代binary serializer(免责声明:由我编写)。尽管它本地支持自定义类型的许多简单类型和集合(因此序列化流中不会存储程序集标识),但是您可能会遇到与问题中出现的相同的问题。

答案 2 :(得分:1)

二进制序列化对DLL Hell有着严肃的态度。它会在序列化数据时记录包含该类型的确切程序集。并且坚持在反序列化数据时找回确切的程序集。只有这样才能确保序列化数据与类型相匹配,采用任何快捷方式只会确保您在幸运垃圾数据时获得异常。这种情况迟早会发生的可能性是100%。

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