对 XmlSerializer 的常见问题进行故障排除

 

克里斯托夫·斯基特科

2004 年 5 月

适用于:
   Microsoft® Visual Studio® .NET

总结:Christoph Schittko 讨论了用于诊断在将 XML 转换为对象时出现的常见问题的各种技术,反之亦然,这些技术在 .NET Framework中使用 XML 序列化技术。 ) (13 个打印页

目录

简介
XmlSerializer 的内部工作
序列化错误
声明序列化类型
反序列化 XML 时出现问题
构造函数的异常
结论
致谢

简介

XmlSerializer.NET Framework 中的 是一个很好的工具,用于将强结构化的 XML 数据映射到 .NET 对象。 使用 XmlSerializer 单个 API 调用在程序中的 XML 文档和对象之间执行转换。 转换的映射规则通过元数据属性在 .NET 类中表示。 此编程模型附带了自己的错误类,开发人员需要了解如何诊断这些错误。 例如,元数据属性必须描述序列化程序可以处理的 XML 格式的所有变体。 本文介绍在使用 XmlSerializer生成基于 XML 的解决方案时可能发生的各种错误,并讨论用于诊断它们的技术和工具。

XmlSerializer 的内部工作

为了有效地解决 XML 序列化引起的问题,请务必了解 在非常简单的 接口 XmlSerializer 的掩护下发生了什么。 与传统分析范例相比, XmlSerializer 来自 System.Xml。.NET Framework中的序列化命名空间将 XML 文档绑定到 .NET 类的实例。 程序员无需编写 DOM 或 SAX 分析代码,而是通过在类中直接附加 .NET 元数据属性,以声明方式设置绑定规则。 由于所有分析规则都通过 属性表示,因此 的 XmlSerializer 接口非常简单。 它主要由两种方法组成: Serialize () 用于从对象实例生成 XML,以及用于将 XML 文档分析为对象图的 Deserialize ()

此方法非常适用于强类型化、刚性结构化 XML 格式的情况,这些格式很好地映射到编程对象。 如果格式由 W3C 架构 定义,该架构由不包含混合内容的 complexTypes 或过度使用通配符 (xs:anyxs;anyAttribute) ,则 XML 序列化是处理该数据的好方法。

面向消息的应用程序是一个很好的示例,其中应用程序之间的交换格式是预先定义的。 由于许多消息驱动的企业应用程序具有非常高的吞吐量要求, Serialize() 因此 和 Deserialize() 方法的设计速度非常快。 事实上, XmlSerializerSystem.Messaging 命名空间中高度可缩放的库的功能,ASP.NET Web 服务和BizTalk Server 2004

的高性能 XmlSerializer 的权衡是双重的。 第一个是给定 XmlSerializer 可以处理的 XML 格式的灵活性,第二个是处理密集型实例构造。

实例化 时, XmlSerializer 必须传递将尝试使用该序列化程序实例进行序列化和反序列化的对象 的类型 。 序列化程序检查 的所有公共字段和属性 Type ,以了解实例在运行时引用的类型。 然后,它继续为一组类创建 C# 代码,以使用 System.CodeDOM 命名空间中的类处理序列化和反序列化。 在此过程中, 检查 XmlSerializer 反射类型的 XML 序列化属性 ,以根据 XML 格式定义自定义创建的类。 然后,这些类编译为临时程序集,并由 Serialize()Deserialize() 方法调用以执行 XML 到对象的转换。

设置 和 声明性编程模型的这一复杂过程 XmlSerializer 会导致三类错误,其中一些错误可能很难进行故障排除:

  • 生成的序列化类需要序列化的对象完全符合元数据属性定义的类型结构。 如果 XmlSerializer 遇到任何未显式声明的类型,或者通过 XML 序列化特性,则 对象将无法序列化。
  • 如果 XML 文档的根元素不映射对象类型,则无法反序列化;当文档格式不正确时(例如,如果文档包含根据 XML 规范非法的字符);在某些情况下,如果文档违反基础架构的限制,则为 。
  • 最后,创建序列化类及其后续编译可能会因多种不同原因而失败。 当传递给构造函数的类型或由该类型引用的类型实现不受支持的接口或不符合 XmlSerializer 施加的限制时,类的创建可能会失败。
    当附加属性生成无法编译的 C# 代码或出于安全相关原因时,编译步骤可能会失败。

以下部分将更深入地研究这些案例,并提供有关如何解决这些问题的指导和建议。

序列化错误

我们检查的第一类错误发生在 方法中 Serialize() 。 当对象图中传递给方法运行时的类型与设计时在 类中声明的类型不匹配时,会发生此错误。 可以通过字段或属性的类型定义隐式声明类型,也可以通过附加序列化特性来显式声明类型。

图 1. 对象图中的类型声明

请务必在此处注意,依赖继承是不够的。 开发人员必须将派生类型声明到 XmlSerializer,方法是将特性附加到 XmlInclude 基类,或者通过将特性附加到 XmlElement 可以保存从声明类型派生的类型对象的字段。

有关示例,请查看此类层次结构:

public class Base
{
   public string Field;
}

public class Derived
{
  public string AnotherField;
}

public class Container
{
  public Base MyField;
}

如果依赖于继承并编写了如下所示的序列化代码:

Container obj = new Container();
obj.MyField = new Derived(); // legal assignment in the 
                             //.NET type system

// ...
XmlSerializer serializer = new XmlSerializer( typeof( Container ) );
serializer.Serialize( writer, obj ); // Kaboom!

方法中会出现异常, Serialize() 因为 没有显式 XmlSerializer类型声明。

XmlSerializer 中的异常

一开始,诊断这些问题的来源可能比较棘手,因为 中的 XmlSerializer 异常似乎没有提供有关其发生原因的大量信息;至少,它们不会在开发人员通常会查看的位置提供信息。

在大多数情况下,当发生错误时, SerializeDeserialize 甚至构造函数都会XmlSerializer引发相当泛型的 System.InvalidOperationException。 此异常类型可能发生在.NET Framework的许多位置;它根本不特定于 XmlSerializer 。 更糟的是,异常的 Message 属性也只生成非常通用的信息。 在上面的示例中, Serialize() 方法将引发异常,并显示以下消息:

There was an error generating the XML document.

此消息充其量很烦人,因为当你看到 XmlSerializer 引发异常时,你已经想得那么多了。 现在,必须发现异常 Message 无助于排查问题。

奇数异常消息和非描述性异常类型反映了本文前面介绍的 XmlSerializer I 的内部工作原理。 方法 Serialize() 捕获序列化类中引发的所有异常,将它们包装在 中 InvalidOperationException, ,然后引发该异常。

读取异常消息

获取“真实”异常信息的技巧是检查异常的 InnerException 属性。 引用 InnerException 从序列化类中引发的实际异常。 它包含有关问题及其发生位置的非常详细的信息。 运行上述示例时要捕获的异常将包含 InnerException 包含以下消息的 :

The type Derived was not expected. Use the XmlInclude or SoapInclude 
attribute to specify types that are not known statically.

可以通过直接检查 InnerException 或通过调用异常的 ToString() 方法获取此消息。 以下代码片段演示了一个异常处理程序,其中写出了在反序列化对象时发生的所有异常中的信息:

public void SerializeContainer( XmlWriter writer, Container obj )
{
  try
  {
    // Make sure even the construsctor runs inside a
    // try-catch block
    XmlSerializer ser = new XmlSerializer( typeof(Container));
    ser.Serialize( writer, obj );
  }
  catch( Exception ex )               
  {                                   
    DumpException( ex );             
  }                                   
}
public static void DumpException( Exception ex )
{
  Console.WriteLine( "--------- Outer Exception Data ---------" );        
  WriteExceptionInfo( ex );
  ex = ex.InnerException;                     
  if( null != ex )               
  {                                   
    Console.WriteLine( "--------- Inner Exception Data ---------" );                
    WriteExceptionInfo( ex.InnerException );    
    ex = ex.InnerException;
  }
}
public static void WriteExceptionInfo( Exception ex )
{
  Console.WriteLine( "Message: {0}", ex.Message );                  
  Console.WriteLine( "Exception Type: {0}", ex.GetType().FullName );
  Console.WriteLine( "Source: {0}", ex.Source );                    
  Console.WriteLine( "StrackTrace: {0}", ex.StackTrace );           
  Console.WriteLine( "TargetSite: {0}", ex.TargetSite );            
}

声明序列化类型

若要修复上述示例中的问题,只需阅读 InnerException的消息并实现建议的解决方案。 传递给 方法的对象图中的字段引用了 Serialize 类型的 Derived对象,但该字段未声明为序列化该 Derived 类型的对象。 即使对象图在 .NET 类型系统中是完全合法的,但 的 XmlSerializer 构造函数在遍历容器类型的字段时不知道为 类型的 Derived 对象创建序列化代码,因为它找不到对类型的任何引用 Derived

若要将字段和属性的其他类型声明到 XmlSerializer,则有多个选项。 可以根据异常消息) 的建议,通过 XmlInclude 属性 (在其基类上声明派生类型,如下所示:

[System.Xml.Serialization.XmlInclude( typeof( Derived ) )]
public class Base
{
    // ...
}

如果 XmlInclude 字段或属性被定义为 Base 类型,则附加特性允许 XmlSerializer 序列化引用 类型对象的 Derived 字段。

或者,只能在单个字段或属性上声明有效类型,而不是在基类中声明派生类型。 可以将 、 XmlAttribute,XmlArrayItem 属性附加到XmlElement字段,并声明字段或属性可以引用的类型。 然后, 的 XmlSerializer 构造函数会将序列化和反序列化这些类型所需的代码添加到序列化类。

读取 StackTrace

MessageInnerException 属性不是携带有价值信息的唯一属性。 属性 StackTrace 传达有关错误来源的更多详细信息。 在堆栈跟踪的最顶部,可找到异常发生位置的方法的名称。 临时程序集中的方法名称遵循序列化类和 Read<n>_<ElementName>反序列化类的模式Write<n>_<ClassName>。 在上述命名空间错误的示例中,会看到异常源自名为 Read1_MyClass的方法。 稍后,我将演示如何使用 Visual Studio 调试器设置断点和单步执行此方法。 但是,首先,让我们看看有关反序列化 XML 文档的常见问题。

反序列化 XML 时出现问题

将 XML 文档反序列化为对象图比将对象图序列化为 XML 更不容易出错。 XmlSerializer当对象与类型定义不紧密匹配时, 非常敏感,但如果反序列化的 XML 文档与 对象不紧密匹配,则非常宽容。 不会对与反序列化对象中的字段或属性不对应的 XML 元素引发异常, XmlSerializer 只是引发 事件。 如果需要跟踪反序列化的 XML 文档与 XML 格式的匹配程度,则可以为这些事件注册处理程序。 但是,无需向 注册事件处理程序 XmlSerializer,以正确处理未映射的 XML 节点。

在反序列化过程中,只有少数错误条件会导致异常。 最常见的容器运行时是:

  • 根元素的名称或其命名空间与预期名称不匹配。
  • 枚举数据类型表示一个未定义的值。
  • 文档包含非法的 XML。

就像序列化一样, Deserialize() 方法会 InvalidOperation 引发消息异常

There is an error in XML document (<line>, <column>).

每当出现问题时。 此异常通常包含 属性中 InnerException 的实际异常。 的类型 InnerException 因读取 XML 文档时发生的实际错误而异。 如果序列化程序无法将文档的根元素与传递给构造函数的类型、通过XmlInclude特性指定的类型或在传递给构造函数的较复杂重载XmlSerializer之一中指定的Type[]类型匹配,则 InnerExceptionInvalidCastException。 请记住, XmlSerializer 正在查看 Qname,即元素的名称和命名空间,以确定要反序列化文档的类。 两者必须匹配 .NET 类中的声明,以便 XmlSerializer 正确标识与文档的根元素对应的类型。

我们来看一个示例:

[XmlRoot( Namespace="urn:my-namespace" )]
public class MyClass
{
  public string MyField;
}

反序列化以下 XML 文档将导致异常,因为 元素的 MyClass XML 命名空间不是 urn:my-namespace, 通过 XmlRoot .NET 类上的 属性声明的:

<MyClass>
  <MyField>Hello, World</MyField>
</MyClass>

让我们仔细看看异常。 异常 Message 比从 Serialize() 方法捕获的消息更具描述性;至少它引用了文档中导致 Deserialize() 失败的位置。 但是,在处理大型 XML 文档时,查看文档并确定错误可能并不容易。 同样, InnerException 提供了更好的信息。 这一次, 它说:

<MyClass xmlns=''> was not expected.

消息仍然有些不明确,但它确实将你指向导致问题的元素。 可以返回并仔细检查 类, MyClass 并将元素名称和 XML 命名空间与 .NET 类中的 XML 序列化属性进行比较。

反序列化无效 XML

另一个经常报告的问题是无法反序列化无效的 XML 文档。 XML 规范禁止在 XML 文档中使用某些 控制字符 。 不过,有时仍会收到包含这些字符的 XML 文档。 问题表现在 (你猜到)InvalidOperationException 中。 不过,在此特定情况下, InnerException 的类型为 XmlExceptionInnerException的消息指向以下点:

hexadecimal value <value>, is an invalid character

如果使用将其规范化属性设置为 false 的 反序列化XmlTextReader,则可以避免此问题。 遗憾的是, XmlTextReader ASP.NET Web 服务 Normalization 在封面下使用的 属性设置为 true;即,它不会反序列化包含这些无效字符的 SOAP 消息。

构造函数的异常

本文讨论的最后一类问题发生在 的构造函数 XmlSerializer 反射到传入的类型上时。 请记住,构造函数以递归方式检查类型层次结构中的每个公共字段和属性,以创建处理序列化和反序列化的类。 然后,它会动态编译类并加载生成的程序集。

在这个复杂的过程中,可能会出现许多不同的问题:

  • 根的声明类型或属性或字段引用的类型不提供默认构造函数。
  • 层次结构中的类型实现集合接口 Idictionary
  • 在对象图中执行某个类型的构造函数或属性访问器需要提升的安全特权。
  • 生成的序列化类的代码不会编译。

尝试将不可序列化的类型传递给 XmlSerializer 构造函数也会生成 InvalidOperationException,但这次异常不会包装另一个异常。 属性 Message 包含有关构造函数拒绝 Type 中传递的 的原因的很好的说明。 尝试序列化未实现没有参数的构造函数的类的实例 (默认构造函数) 会导致异常 Message

Test.NonSerializable cannot be serialized because it does not have a default public constructor.

另一方面,排查编译错误非常复杂。 这些问题在 中 FileNotFoundException 自行表现出来,并显示以下消息:

File or assembly name abcdef.dll, or one of its dependencies, was not found. File name: "abcdef.dll"
   at System.Reflection.Assembly.nLoad( ... )
   at System.Reflection.Assembly.InternalLoad( ... )
   at System.Reflection.Assembly.Load(...)
   at System.CodeDom.Compiler.CompilerResults.get_CompiledAssembly() 
    ....

你可能想知道未找到文件异常与实例化序列化程序对象有什么关系,但请记住:构造函数写入 C# 文件并尝试编译它们。 此异常的调用堆栈提供了一些很好的信息来支持这种怀疑。 尝试加载通过CodeDOM调用 System.Reflection.Assembly.Load 方法生成的程序集时XmlSerializer发生异常。 该异常未解释应创建的程序集 XmlSerializer 不存在的原因。 一般情况下,程序集不存在是因为编译失败,这可能是因为,在极少数情况下,序列化属性生成 C# 编译器无法编译的代码。

注意 当 在 XmlSerializer 无法访问临时目录的帐户或安全环境中运行时,也会发生此错误。

实际编译错误不是 引发 XmlSerializer的任何异常错误消息的一 InnerException部分,甚至不是 。 因此,在 Chris Sells 发布其 XmlSerializerPrecompiler 工具之前,很难排查这些异常。

The XmlSerializerPreCompiler

XmlSerializer PreCompiler 是一个命令行程序,可执行与 的构造函数XmlSerializer相同的步骤。 它在类型上反映、生成序列化类并对其进行编译,并且由于它纯粹设计为故障排除工具,因此该工具可将任何编译错误写入控制台是安全的。

该工具非常易于使用。 只需将工具指向包含导致异常的类型的程序集,并指定要预编译的类型。 接下来举例说明。 将 或 和 XmlArrayItem 属性附加到XmlElement定义为交错数组的字段时,经常会出现一个问题,如以下示例所示:

namespace Test
{
  public class StringArray
  {
    [XmlElement( "arrayElement", typeof( string ) )]
    public string [][] strings;
  }
}

当你XmlSerializer实例化XmlSerializer类型的 Test.StringArray对象时,构造函数会引发 FileNotFoundException 。 如果编译 类并尝试序列化它的实例,你将获得 FileNotFoundException,但不会获得有关问题真实性质的线索。 XmlSerializerPreCompiler可以为你提供缺失的信息。 在我的示例中, StringArray 类被编译为名为 XmlSer.exe程序集,我必须使用以下命令行运行该工具:

XmlSerializerPreCompiler.exe XmlSer.exe Test.StringArray

第一个命令行参数指定程序集,第二个参数定义程序集中要预编译的类。 该工具会将大量信息写入命令窗口。

图 2. XmlSerializerPreCompiler 命令窗口输出

要查看的重要行是包含编译错误的行和两个读取如下内容的行:

XmlSerializer-produced source:
C:\DOCUME~1\<user>\LOCALS~1\Temp\<random name>.cs

现在, XmlSerializerPreCompiler 提供了编译错误以及源文件的位置以及不编译的代码。

调试序列化代码

在正常情况下,XmlSerializer 会在不再需要序列化类的 C# 源文件时删除这些文件。 但是,有一个未记录的诊断开关,它将指示 XmlSerializer 删除以将这些文件保留在磁盘上。 可以在应用程序的 .config 文件中设置 开关:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.diagnostics>
        <switches>
            <add name="XmlSerialization.Compilation" value="4" />
        </switches>
    </system.diagnostics>
</configuration>

在.config文件中存在此开关后,C# 源文件将保留在临时目录中。 如果在运行 Windows 2000 或更高版本的计算机上工作,则临时目录的默认位置为 <System Drive>\Documents and Settings\<Your User Name>\LocalSettings\Temp 或 <Windows Directory>\Temp,适用于在 ASPNET 帐户下运行的 Web 应用程序。 C# 文件很容易遗漏,因为它们具有看起来非常奇怪的随机生成的文件名,类似于 bdz6lq-t.0.cs。 设置XmlSerializerPreCompiler此诊断开关,以便你可以打开文件以检查记事本或 Visual Studio 中报告的编译错误的行XmlSerializerPreCompiler

你甚至可以单步执行这些临时序列化类,因为诊断开关还会在磁盘上留下带有调试符号的 .pdb 文件。 如果需要在序列化类中设置断点,则可以在 Visual Studio 调试器下运行应用程序。 在应用程序加载的输出窗口中看到消息后,从临时目录中显示具有这些奇怪名称的程序集,然后打开具有相应名称的 C# 文件并设置断点,就像在自己的代码中一样。

图 3. 诊断开关的编译错误输出

在序列化类中设置断点后,需要对 对象执行调用 Serialize()Deserialize() 方法 XmlSerializer 的代码。

注意 只能调试序列化和反序列化,而不能调试构造函数中运行的代码生成过程。

单步执行序列化类,可以查明每个序列化问题。 如果要单步执行 SOAP 消息的反序列化,可以使用该技巧,因为 ASP.NET Web 服务和 Web 服务代理是在 的基础上构建的 XmlSerializer。 只需将诊断开关添加到配置文件,并在 类中设置一个用于反序列化消息的断点。 如果 WSDL 在生成代理类时未准确反映消息格式,我会使用此方法来确定正确的序列化属性集。

结论

这些提示应有助于诊断 的 XmlSerializer序列化问题。 你遇到的大多数问题都源于 XML 序列化属性的错误组合或与要反序列化的类型不匹配的 XML。 序列化属性控制序列化类的代码生成,并可能导致编译错误或运行时异常。 仔细检查 引发 XmlSerializer 的异常将有助于识别运行时异常的来源。 如果需要更深入地诊断问题,则 XmlSerializerPreCompiler 工具可帮助你查找编译错误。 如果这两种方法都没有导致问题的根本原因,则可以检查自动创建的序列化类的代码,并在调试器中逐行执行它们。

致谢

我要感谢 达雷·奥巴桑乔丹尼尔·卡祖利诺 对本文的反馈和编辑建议。