.Net二进制序列化,无需引用源类型的程序集或如何与BinaryFormatter协商

在本文中,我将共享程序集之间的二进制类型序列化的经验,而无需互相引用。 事实证明,在某些实际情况下,当您需要反序列化数据而无需链接到声明了该程序集的链接时,便会遇到这种情况。 在本文中,我将讨论所需的方案,将描述解决方案的方法,还将讨论在搜索过程中出现的中间错误。

引言 问题陈述


我们与一家在地质领域工作的大型公司合作。 从历史上看,该公司编写了非常不同的软件来处理来自不同类型设备+数据分析+预测的数据。 this,所有这些软件都不总是彼此“友好”,更多时候甚至根本不友好。 为了以某种方式整合信息,现在正在创建一个Web门户,其中不同的程序以xml的形式上传其数据。 门户网站正在尝试创建正负完整视图。 一个重要的细微差别:由于门户的开发人员在每个应用程序的主题领域都不强,因此每个团队都提供了从其xml到门户数据结构的解析器/数据转换器模块。

我在一个团队中开发应用程序之一,我们很容易为部分数据编写了导出机制。 但是在这里,业务分析师决定中央门户需要我们计划正在构建的报告之一。 这是第一个问题出现的地方:每次都重新生成报告,并且结果不会保存在任何地方。
“所以保存吧!” 读者可能会想。 我也这么认为,但是对于已经为下载的数据生成报告的要求感到非常失望。 没什么可做的-您需要转移逻辑。

阶段0。重构。 没什么好麻烦的


已决定将构建报告的逻辑(实际上,这是一个4列标签,但逻辑是一个小车和一个大推车)分成一个单独的类,并在解析器程序集中通过引用将该类的文件包括在内。 通过这个我们:

  1. 避免直接复制
  2. 防止版本差异

将逻辑划分为单独的类并不是一件容易的事。 但是随后一切都变得不那么乐观:该算法基于业务对象,业务对象的传输不适合我们的概念。 我不得不重写这些方法,以便它们仅接受简单类型并对其进行操作。 它并不总是那么简单,并且在某些地方需要解决方案,但其美观性尚有待商,,但总的来说,在没有明显拐杖的情况下仍可得到可靠的解决方案。

如您所知,有一个细节经常充当魔鬼的避难所:我们继承了前几代开发人员的一种奇怪方法,根据该方法,构建报告所需的某些数据以二进制序列化.Net对象的形式存储在数据库中(由于缺少收件人,“为什么?”,“ kaaak?”之类的问题将无法得到答复)。 当然,在计算的输入中,我们必须对它们进行反序列化。

这些类型是无法摆脱的,我们还包括“通过引用”,特别是因为它们并不复杂。

阶段1.反序列化。 记住全名


完成上述操作并执行测试运行后,我意外收到了运行时错误,
[A] Namespace.TypeA无法转换为[B] Namespace.TypeA。 类型A源自位置“ ...”处上下文“默认”中的“ Assembley.Application,版本= 1.0.0.0,文化=中性,PublicKeyToken = null”。 类型B源自位置``默认''上下文中的'Assmbley.Portal,版本= 1.0.0.0,文化=中性,PublicKeyToken = null'。
最初的Google链接告诉我,事实是BinaryFormatter不仅将数据写入类型信息,而且还将类型信息写入输出流,这是合乎逻辑的。 考虑到类型的全名包含声明它的程序集,从.Net的角度来看,我试图反序列化一种类型的图完全不同。

碰到我的头后,我碰巧做出了一个显而易见的决定,但可恶的是,决定在动态反序列化期间替换特定的TypeA类型。 一切正常。 报告的结果从上到下收敛,通过了构建服务器上的测试。 带着成就感,我们将任务发送给测试人员。

第二阶段。主要。 程序集之间的序列化


估算很快以测试人员注册的错误的形式出现,该错误表明门户网站侧的解析器崩溃了,但它无法加载程序集Assembley.Application(来自应用程序的程序集)。 最初的想法-我没有清理参考文献。 但是-不,一切都很好,没有人提及。 我尝试在沙箱中再次运行它-一切正常。 我开始怀疑构建错误,但是在这里,我想到一个不满意的想法:我将解析器的输出路径更改为单独的文件夹,而不是应用程序的常规bin目录。 瞧-我得到了描述的异常。 Stectrace分析证实了模糊的猜测-反序列化正在下降。

这种认识是快速而痛苦的:用动态替换特定类型不会改变任何事情,BinaryFormatter仍然从外部程序集创建类型,只有当具有该类型的程序集在附近,运行时自然地加载它并且程序集消失时-我们得到一个错误。

有理由感到难过。 但是谷歌搜索以SerializationBinder类的形式给了希望。 事实证明,它使您可以确定对数据进行反序列化的类型。 为此,创建一个继承人并在其中定义以下方法。

public abstract Type BindToType(String assemblyName, String typeName); 

您可以在其中返回给定条件的任何类型。
BinaryFormatter类具有Binder属性,您可以在其中注入实现。

似乎没有问题。 但是同样,细节仍然存在(见上文)。

首先,您必须处理所有类型(以及标准类型)的请求。
此处的Internet上找到了一个有趣的实现选项,但他们正在尝试使用BinaryFormatter的默认绑定器,其构造形式

 var defaultBinder = new BinaryFormatter().Binder 

但实际上,默认情况下,Binder属性为null。 对源代码的分析表明,在BinaryFormatter内部,是否检查了Binder,如果已检查,则调用其方法;如果未检查,则使用内部逻辑,最终归结为

  var assembly = Assembly.Load(assemblyName); return FormatterServices.GetTypeFromAssembly(assembly, typeName); 

事不宜迟,我自己重复了同样的逻辑。

这是第一个实现中发生的事情

 public class MyBinder : SerializationBinder { public override Type BindToType(string assemblyName, string typeName) { if (assemblyName.Contains("<ObligatoryPartOfNamespace>") ) { var bindToType = Type.GetType(typeName); return bindToType; } else { var bindToType = LoadTypeFromAssembly(assemblyName, typeName); return bindToType; } } private Type LoadTypeFromAssembly(string assemblyName, string typeName) { if (string.IsNullOrEmpty(assemblyName) || string.IsNullOrEmpty(typeName)) return null; var assembly = Assembly.Load(assemblyName); return FormatterServices.GetTypeFromAssembly(assembly, typeName); } } 

即 检查名称空间是否属于项目-我们从当前域返回类型,如果系统类型-我们从相应程序集加载

看起来很合逻辑。 我们开始测试:我们的类型来了-我们替换,它被创建。 万岁! 字符串来了-我们沿着分支加载程序集。 有效! 打开虚拟香槟...

但是在这里...词典带有用户类型的元素:由于这是系统类型,因此...显然,我们正尝试从程序集中加载它,但是由于它具有的元素是我们的类型,而且具有完全限定(程序集,版本,键),那么我们又跌倒了。 (应该有一个悲伤的微笑)。

显然,您需要更改类型的输入名称,替换为所需程序集的链接。 我真的希望类型名称有一个AssemblyName类的类似物,但是我没有发现任何类似的东西。 编写具有替换功能的通用解析器并非易事。 经过一系列实验,我得出以下解决方案:在静态构造函数中,我减去要替换的类型,然后在具有创建类型名称的行中查找它们的名称,并在找到时替换程序集名称。

  /// <summary> /// The types that may be changed to local /// </summary> protected static IEnumerable<Type> _changedTypes; static MyBinder() { var executingAssembly = Assembly.GetCallingAssembly(); var name = executingAssembly.GetName().Name; _changedTypes = executingAssembly.GetTypes().Where(t => t.Namespace != null && !t.Namespace.Contains(name) && !t.Name.StartsWith("<")); //!t.Namespace.Contains(name) - .     ,         // "<'      -     } private static string CorrectTypeName(string name) { foreach (var changedType in _changedTypes) { var ind = name.IndexOf(changedType.FullName); if (ind != -1) { var endIndex = name.IndexOf("PublicKeyToken", ind) ; if (endIndex != -1) { endIndex += +"PublicKeyToken".Length + 1; while (char.IsLetterOrDigit(name[endIndex++])) { } var sb = new StringBuilder(); sb.Append(name.Substring(0, ind)); sb.Append(changedType.AssemblyQualifiedName); sb.Append(name.Substring(endIndex-1)); name = sb.ToString(); } } } return name; } /// <summary> /// look up the type locally if the assembly-name is "NA" /// </summary> /// <param name="assemblyName"></param> /// <param name="typeName"></param> /// <returns></returns> public override Type BindToType(string assemblyName, string typeName) { typeName = CorrectTypeName(typeName); if (assemblyName.Contains("<ObligatoryPartOfNamespace>") || assemblyName.Equals("NA")) { var bindToType = Type.GetType(typeName); return bindToType; } else { var bindToType = LoadTypeFromAssembly(assemblyName, typeName); return bindToType; } } 

如您所见,我首先从PublicKeyToken是类型描述中的最后一个事实开始。 也许这不是100%可靠的,但是在我的测试中,我没有发现并非如此的情况。

因此,形式为
“ System.Collections.Generic.Dictionary`2 [[SomeNamespace.CustomType,Assembley.Application,版本= 1.0.0.0,文化=中性,PublicKeyToken =空],[System.Byte [],mscorlib,版本= 4.0.0.0,文化=中性,PublicKeyToken = b77a5c561934e089]]»»

变成
“ System.Collections.Generic.Dictionary`2 [[SomeNamespace.CustomType,Assembley.Portal,版本= 1.0.0.0,区域性=中性,PublicKeyToken =空],[System.Byte [],mscorlib,版本= 4.0.0.0,文化=中性,PublicKeyToken = b77a5c561934e089]]»»

现在,一切最终都像时钟一样工作。 技术上有些细微的差别:如果您还记得,我们包含的文件包含在主应用程序的链接中。 但是在主要应用中,不需要所有这些舞蹈。 因此,形式的条件编译机制

 BinaryFormatter binForm = new BinaryFormatter(); #if EXTERNAL_LIB binForm.Binder = new MyBinder(); #endif 

因此,在门户网站程序集中,我们定义了宏EXTERNAL_LIB,但是在主应用程序中-否

“非抒情离题”


实际上,在编码过程中,为了快速检查解决方案,我进行了一次计算错误,这可能使我花了一定数量的神经细胞:对于初学者,我只是硬编码了Dicitionary的类型替换。 结果,在反序列化之后,它变成了一个空的Dictionary,在尝试对其执行某些操作时也会“崩溃”。 我已经开始认为您无法欺骗BinaryFormatter ,并且我开始进行绝望的尝试,试图编写Dictionary的继承人。 幸运的是,我几乎及时停止了工作,回到编写通用替换机制的过程中,我意识到创建一个字典并不足以重新定义其类型:您仍然需要注意KeyValuePair <TKey,TValue>,Comparer的类型,活页夹


这些是二进制序列化冒险。 谢谢您的反馈。

Source: https://habr.com/ru/post/zh-CN430646/


All Articles