当 IXmlSerializable.ReadXml() 内发生架构验证错误时,为什么 XmlSerializer 会抛出异常并引发 ValidationEvent [英] Why does XmlSerializer throws an Exception and raise a ValidationEvent when a schema validation error occurs inside IXmlSerializable.ReadXml()

查看:26
本文介绍了当 IXmlSerializable.ReadXml() 内发生架构验证错误时,为什么 XmlSerializer 会抛出异常并引发 ValidationEvent的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我编写了一些测试来读取 XML 文件并根据 XSD 架构对其进行验证.我的数据对象混合使用基于属性和自定义的 IXmlSerializable 实现,我使用 XmlSerializer 执行反序列化.

我的测试涉及在 XML 中插入一个未知元素,使其不符合架构.然后我测试验证事件是否触发.

如果未知元素被放置在 XML 中,因此它是基于属性的数据类之一的子元素(即属性用 XmlAttribute 和 XmlElement 属性修饰),则验证会正确触发.

但是,如果未知元素被放置在 XML 中,因此它是 IXmlSerializable 类之一的子级,则抛出 System.InvalidOperationException,但验证仍会触发.

自定义集合的 ReadXmlElements 中的代码创建了一个新的 XmlSerializer 以读取子项,它是引发 InvalidOperationException 的 Deserialize 调用.

如果我在这个调用周围放置一个 try .. catch 块,它就会陷入无限循环.唯一的解决方案似乎是在顶级 XmlSerializer.Deserialize 调用周围放置一个 try-catch 块(如测试中所示).

有谁知道 XmlSerializer 为什么会这样?理想情况下,我想尝试捕获抛出的异常,而不是拥有顶级异常处理程序,因此还有一个次要问题,即如果 try..catch 块是为什么代码会陷入无限循环添加到集合类中.

这里是抛出的异常:

System.InvalidOperationException:XML 文档中存在错误 (13, 10).--->System.InvalidOperationException: XML 文档中存在错误 (13, 10).--->System.InvalidOperationException: 没想到.在 Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationReaderGroup.Read1_Group()--- 内部异常堆栈跟踪结束 ---在 System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader, String encodingStyle, XmlDeserializationEvents 事件)在 System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader)在 XmlSerializerTest.EntityCollection~1.ReadXmlElements(XmlReader reader) 在 C:\source\repos\XmlSerializerTest\XmlSerializerTest\EntityCollection.cs:line 55在 XmlSerializerTest.EntityCollection~1.ReadXml(XmlReader reader) 在 C:\Users\NGGMN9O\source\repos\XmlSerializerTest\XmlSerializerTest\EntityCollection.cs:line 41在 System.Xml.Serialization.XmlSerializationReader.ReadSerializable(IXmlSerializable serializable, BooleanwrappedAny)在 Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationReaderExample.Read2_Example(Boolean isNullable, Boolean checkType)在 Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationReaderExample.Read3_Example()--- 内部异常堆栈跟踪结束 ---在 System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader, String encodingStyle, XmlDeserializationEvents 事件)在 System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader)在 XmlSerializerTest.StackOverflowExample.InvalidElementInGroupTest() 在 C:\source\repos\XmlSerializerTest\XmlSerializerTest\XmlSerializerTest.cs:line 35

Schema.xsd

<!-- 属性组--><xs:attributeGroup name="标识符"><xs:属性名称="Id"类型=xs:字符串"使用=必需"/><xs:attribute name="姓名"类型=xs:字符串"使用=必需"/></xs:attributeGroup><!-- 复杂类型--><xs:complexType abstract="true"名称="实体"><xs:序列><xs:element name="描述"类型=xs:字符串"minOccurs="0"maxOccurs="1"/></xs:sequence><xs:attributeGroup ref="local:Identifiers"/></xs:complexType><xs:complexType name="DerivedEntity"><xs:complexContent><xs:extension base="local:Entity"><xs:attribute name="参数"使用=必需"/></xs:extension></xs:complexContent></xs:complexType><xs:complexType name="Groups"><xs:序列><xs:element name="Group" type="local:Group" minOccurs="0" maxOccurs="unbounded"/></xs:sequence></xs:complexType><xs:complexType name="Group"><xs:complexContent><xs:extension base="local:Entity"><xs:序列><xs:element name="DerivedEntity"类型=本地:派生实体"minOccurs="0"maxOccurs="无界"/></xs:sequence></xs:extension></xs:complexContent></xs:complexType><!-- 主架构定义--><xs:element name="示例"><xs:complexType><xs:序列><xs:element name="组"类型=本地:组"minOccurs="1"maxOccurs="1"/></xs:sequence></xs:complexType></xs:element></xs:schema>

InvalidElementInGroup.xml

<示例 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="example"><组><组名="abc" Id="123"><DerivedEntity Id="123" Name="xyz" Parameter="ijk"><描述>定义</描述></DerivedEntity><DerivedEntity Id="234" Name="bob" Parameter="12"/></组><组名="def" Id="124"><描述>这是一个描述.</描述></组><未知元素/></组></示例>

实施注意:本示例中显示的代码不是生产代码.我知道我可以只使用支持序列化的 List 实现,而无需实现 IXmlSerializable.

使用系统;使用 System.Collections;使用 System.Collections.Generic;使用 System.Xml;使用 System.Xml.Schema;使用 System.Xml.Serialization;命名空间 XmlSerializerTest{公共课示例{公共示例(){组 = 新组 ();}公共组组 { 获取;放;}}公共类组:EntityCollection{}公共类组:实体,IXmlSerializable{私有 EntityCollection实体集合;公共组(){this.entityCollection = new EntityCollection();}#region IXmlSerializable 实现公共 XmlSchema GetSchema(){返回空;}public void ReadXml(XmlReader reader){reader.MoveToContent();//读取属性ReadXmlAttributes(reader);//消费起始元素bool isEmptyElement = reader.IsEmptyElement;reader.ReadStartElement();如果 (!isEmptyElement){ReadXmlElements(reader);reader.ReadEndElement();}}///<总结>///读取 XML 元素.///</总结>///<param name="reader">阅读器.</param>公共覆盖 void ReadXmlElements(XmlReader reader){//处理可选的基类描述元素base.ReadXmlElements(reader);entityCollection.ReadXmlElements(reader);}public void WriteXml(XmlWriter writer){抛出新的 NotImplementedException();}#endregion}公共类 EntityCollection<T>: IXmlSerializable, IList其中 T : 实体{私人列表<T>子实体字段;公共实体集合(){childEntityField = new List();}#region IXmlSerializable 实现公共 XmlSchema GetSchema(){返回空;}public void ReadXml(XmlReader reader){reader.MoveToContent();//读取属性ReadXmlAttributes(reader);//消费起始元素bool isEmptyElement = reader.IsEmptyElement;reader.ReadStartElement();如果 (!isEmptyElement){ReadXmlElements(reader);reader.ReadEndElement();}}public virtual void ReadXmlAttributes(XmlReader reader){}public virtual void ReadXmlElements(XmlReader reader){XmlSerializer deserializer = new XmlSerializer(typeof(T), "example");while (reader.IsStartElement()){T item = (T)deserializer.Deserialize(reader);//如果遇到未知元素,则抛出 InvalidOperationException.如果(项目!= null){新增项目);}}}public void WriteXml(XmlWriter writer){抛出新的 NotImplementedException();}#endregion#region IList 实现公共 IEnumerator<T>获取枚举器(){返回 childEntityField.GetEnumerator();}IEnumerator IEnumerable.GetEnumerator(){返回 ((IEnumerable)childEntityField).GetEnumerator();}公共无效添加(T项){childEntityField.Add(item);}公共无效清除(){childEntityField.Clear();}public bool 包含(T 项){返回 childEntityField.Contains(item);}public void CopyTo(T[] array, int arrayIndex){childEntityField.CopyTo(array, arrayIndex);}public bool Remove(T 项){返回 childEntityField.Remove(item);}公共整数计数 =>childEntityField.Count;public bool IsReadOnly =>((ICollection)childEntityField).IsReadOnly;public int IndexOf(T item){返回 childEntityField.IndexOf(item);}公共无效插入(整数索引,T项){childEntityField.Insert(index, item);}public void RemoveAt(int index){childEntityField.RemoveAt(index);}public T this[int index]{得到 =>childEntityField[索引];设置 =>childEntityField[index] = 值;}#endregion}[System.Xml.Serialization.XmlIncludeAttribute(typeof(DerivedEntity))]公共抽象类实体{公共字符串 描述 { get;放;}公共字符串 ID { 获取;放;}公共字符串名称 { 获取;放;}public virtual void ReadXmlAttributes(XmlReader reader){Id = reader.GetAttribute("Id");Name = reader.GetAttribute("Name");}public virtual void ReadXmlElements(XmlReader reader){if (reader.IsStartElement("Description")){描述 = reader.ReadElementContentAsString();}}}公共类派生实体:实体{公共字符串参数{获取;放;}}}

测试

命名空间 XmlSerializerTest{使用系统;使用 System.IO;使用 System.Xml;使用 System.Xml.Schema;使用 System.Xml.Serialization;使用 Microsoft.VisualStudio.TestTools.UnitTesting;[测试班]公共类 StackOverflowExample{[测试方法][DeploymentItem(@"Schema.xsd")][DeploymentItem(@"InvalidElementInGroup.xml")]public void InvalidElementInGroupTest(){//打开文件FileStream stream = new FileStream("InvalidElementInGroup.xml", FileMode.Open);//配置设置XmlReaderSettings settings = new XmlReaderSettings();settings.Schemas.Add(null, @"Schema.xsd");settings.ValidationType = ValidationType.Schema;settings.ValidationEventHandler += OnValidationEvent;XmlSerializer xmlDeserializer = new XmlSerializer(typeof(Example), "example");//从流中反序列化流.位置= 0;XmlReader xmlReader = XmlReader.Create(stream, settings);尝试{示例 deserializedObject = (Example)xmlDeserializer.Deserialize(xmlReader);}捕获(例外 e){Console.WriteLine("异常:" + e);}}私有无效 OnValidationEvent(对象发送者,ValidationEventArgs e){Console.WriteLine("验证事件:" + e.Message);}}}

解决方案

你的基本问题是你有一个抽象基类Entity,它的继承者有时实现IXmlSerializable 有时不't,当他们这样做时,他们被包含在一个集合中,该集合也实现了 IXmlSerializable 并将集合属性与其 XML 中的集合子项混合在一起.在读取此 XML 的过程中,您没有正确推进 XmlReader 并且反序列化失败.

在实施 IXmlSerializable 时,您需要遵守 此答案中所述的规则 实现 IXmlSerializable 的正确方法? Marc Gravell 以及文档:

对于 IXmlSerializable.WriteXml(XmlWriter):

<块引用>

您提供的 WriteXml 实现应该写出对象的 XML 表示.框架编写一个包装元素并在它开始后定位 XML 编写器.您的实现可能会编写其内容,包括子元素.然后框架关闭包装元素.

对于 IXmlSerializable.ReadXml(XmlReader):

<块引用>

ReadXml 方法必须使用 WriteXml 方法写入的信息重构您的对象.

当这个方法被调用时,阅读器被定位在包装你的类型信息的开始标签上.也就是说,直接在指示序列化对象开始的开始标记上.当此方法返回时,它必须从头到尾读取了整个元素,包括其所有内容.与 WriteXml 方法不同,框架不会自动处理包装元素.您的实现必须这样做.不遵守这些定位规则可能会导致代码产生意外的运行时异常或损坏数据.

特别注意 ReadXml() 必须完全使用容器元素.这在继承场景中被证明是有问题的;是基类负责消耗外部元素还是派生类?此外,如果某些派生类在读取过程中不正确地定位 XmlReader,这可能会被单元测试忽略,但会导致 XML 文件中的后续数据在生产中被忽略或损坏.

因此创建一个用于读写 IXmlSerializable 对象的扩展框架是有意义的,这些对象的基类和派生类都具有自定义(反)序列化逻辑,其中容器元素的处理,每个属性, 并且每个子元素都是分开的:

公共静态类 XmlSerializationExtensions{public static void ReadIXmlSerializable(XmlReader reader, Func handleXmlAttribute, Func handleXmlElement, Func handleXmlText){//https://docs.microsoft.com/en-us/dotnet/api/system.xml.serialization.ixmlserializable.readxml?view=netframework-4.8#remarks//当这个方法被调用时,阅读器被定位在包装你的类型信息的开始标签上.//也就是直接在开始标记上,指示序列化对象的开始.//当这个方法返回时,它必须从头到尾读取了整个元素,包括它的所有内容.//与WriteXml方法不同,框架不会自动处理包装元素.您的实现必须这样做.//不遵守这些定位规则可能会导致代码产生意外的运行时异常或损坏数据.reader.MoveToContent();if (reader.NodeType != XmlNodeType.Element)throw new XmlException(string.Format("Invalid NodeType {0}", reader.NodeType));如果(reader.HasAttributes){for (int i = 0; i  writeAttributes, Action writeNodes){//https://docs.microsoft.com/en-us/dotnet/api/system.xml.serialization.ixmlserializable.writexml?view=netframework-4.8#remarks//你提供的WriteXml实现应该写出对象的XML表示.//框架编写一个包装元素并在其开始后定位 XML 编写器.您的实现可能会编写其内容,包括子元素.//框架然后关闭包装器元素.写属性(作家);写节点(作家);}}

然后,按如下方式修改您的数据模型:

公共类常量{public const string ExampleNamespace = "example";}[XmlRoot(Namespace = Constants.ExampleNamespace)]公共课示例{公共示例(){组 = 新组 ();}公共组组 { 获取;放;}}公共类组:EntityCollection{}公共类 EntityCollection<T>: IXmlSerializable, IList其中 T : 实体{私人列表<T>子实体字段;公共实体集合(){childEntityField = new List();}#region IXmlSerializable 实现public XmlSchema GetSchema() { return null;}受保护的内部虚拟 bool HandleXmlAttribute(XmlReader reader) { return false;}受保护的内部虚拟 void WriteAttributes(XmlWriter writer) { }受保护的内部虚拟 bool HandleXmlElement(XmlReader reader){var serializer = new XmlSerializer(typeof(T), Constants.ExampleNamespace);if (serializer.CanDeserialize(reader)){T item = (T)serializer.Deserialize(reader);如果(项目!= null)新增项目);返回真;}返回假;}受保护的内部虚拟 void WriteNodes(XmlWriter writer){var serializer = new XmlSerializer(typeof(T), Constants.ExampleNamespace);foreach (var item in this){serializer.Serialize(writer, item);}}public void ReadXml(XmlReader reader){XmlSerializationExtensions.ReadIXmlSerializable(reader, r => HandleXmlAttribute(r), r => HandleXmlElement(r), r => false);}public void WriteXml(XmlWriter writer){XmlSerializationExtensions.WriteIXmlSerializable(writer, w => WriteAttributes(w), w => WriteNodes(w));}#endregion#region IList 实现公共 IEnumerator<T>获取枚举器(){返回 childEntityField.GetEnumerator();}IEnumerator IEnumerable.GetEnumerator(){返回 ((IEnumerable)childEntityField).GetEnumerator();}公共无效添加(T项){childEntityField.Add(item);}公共无效清除(){childEntityField.Clear();}public bool 包含(T 项){返回 childEntityField.Contains(item);}public void CopyTo(T[] array, int arrayIndex){childEntityField.CopyTo(array, arrayIndex);}public bool Remove(T 项){返回 childEntityField.Remove(item);}public int Count { get { return childEntityField.Count;} }public bool IsReadOnly { get { return ((ICollection)childEntityField).IsReadOnly;} }public int IndexOf(T item){返回 childEntityField.IndexOf(item);}公共无效插入(整数索引,T项){childEntityField.Insert(index, item);}public void RemoveAt(int index){childEntityField.RemoveAt(index);}public T this[int index]{得到 { 返回 childEntityField[index];}设置 { childEntityField[index] = 值;}}#endregion}公共类组:实体,IXmlSerializable{私有 EntityCollection实体集合;公共组(){this.entityCollection = new EntityCollection();}#region IXmlSerializable 实现公共 XmlSchema GetSchema(){返回空;}protected override bool HandleXmlElement(XmlReader reader){if (base.HandleXmlElement(reader))返回真;返回 entityCollection.HandleXmlElement(reader);}protected override void WriteNodes(XmlWriter writer){base.WriteNodes(writer);entityCollection.WriteNodes(writer);}protected override bool HandleXmlAttribute(XmlReader reader){if (base.HandleXmlAttribute(reader))返回真;if (entityCollection.HandleXmlAttribute(reader))返回真;返回假;}protected override void WriteAttributes(XmlWriter writer){base.WriteAttributes(writer);entityCollection.WriteAttributes(writer);}public void ReadXml(XmlReader reader){XmlSerializationExtensions.ReadIXmlSerializable(reader, r => HandleXmlAttribute(r), r => HandleXmlElement(r), r => false);}public void WriteXml(XmlWriter writer){XmlSerializationExtensions.WriteIXmlSerializable(writer, w => WriteAttributes(w), w => WriteNodes(w));}#endregion}公共类派生实体:实体{[Xml 属性]公共字符串参数{获取;放;}}[System.Xml.Serialization.XmlIncludeAttribute(typeof(DerivedEntity))]公共抽象类实体{[XmlElement]公共字符串 描述 { get;放;}[Xml 属性]公共字符串 ID { 获取;放;}[Xml 属性]公共字符串名称 { 获取;放;}protected virtual void WriteAttributes(XmlWriter writer){如果(ID!= null)writer.WriteAttributeString("Id", Id);如果(名称!= null)writer.WriteAttributeString("Name", Name);}protected virtual bool HandleXmlAttribute(XmlReader reader){if (reader.LocalName == "Id"){Id = reader.Value;返回真;}else if (reader.LocalName == "Name"){名称 = reader.Value;返回真;}返回假;}protected virtual void WriteNodes(XmlWriter writer){如果(说明!= null){writer.WriteElementString("描述", 描述);}}protected virtual bool HandleXmlElement(XmlReader reader){if (reader.LocalName == "描述"){描述 = reader.ReadElementContentAsString();返回真;}返回假;}}

并且您将能够成功地反序列化和重新序列化Example.演示小提琴 此处.

注意事项:

  • 认真考虑简化这个架构.这太复杂了.

  • 将在 内为 正确引发单个验证事件,因为架构中没有出现此类元素.

  • XmlSerializer.Deserialize() 将在根 XML 元素名称和命名空间与预期名称和命名空间不匹配时抛出 InvalidOperationException.您可以通过调用 XmlSerializer.CanDeserialize(XmlReader).

  • 请务必测试带有和不带有缩进的 XML 反序列化.有时,ReadXml() 方法会使读者提前一个节点,但如果 XML 包含无关紧要的缩进(即格式),则不会造成任何伤害,因为只会跳过无关紧要的空白节点.p>

  • 当在派生类中重写 Entity.HandleXmlElement(XmlReader reader) 时,应首先调用基类方法.如果基类方法处理该元素,则返回 true 并且派生类不应尝试处理它.类似地,如果派生类处理元素,true 应该返回给更多派生类,指示元素已被处理.false 当类和基类都不能处理元素时返回.

  • XmlReader.ReadSubtree() 可用于确保某些派生类不会错位 HandleXmlElement(XmlReader reader)XmlReader>.

  • 如果您使用 new XmlSerializer(Type)new XmlSerializer(Type, String) 构造一个XmlSerializer,你必须只构造一次并静态缓存它以避免严重的内存泄漏.有关原因,请参阅 文档使用 StreamReader 和 XmlSerializer 的内存泄漏.您没有在示例代码中以这种方式构建序列化程序,但可能在您的生产代码中这样做.

I have written some tests for reading an XML file and validating it against an XSD schema. My data objects are using a mix of attribute based and custom IXmlSerializable implementation and I am using the XmlSerializer to perform deserialization.

My test involves inserting an unknown element into the XML so that it does not conform to the schema. I then test if the validation event fires.

If the unknown element is placed in the XML so it's a child of one of the attribute based data classes (i.e. the properties are decorated with XmlAttribute and XmlElement attributes), then the validation fires correctly.

If however, the unknown element is placed in the XML so it's a child of one of the IXmlSerializable classes, then a System.InvalidOperationException is thrown, but the validation does still fire.

The code inside the custom collection's ReadXmlElements creates a new XmlSerializer to read in the child items, it is the Deserialize call where the InvalidOperationException is thrown.

If I place a try .. catch block around this call, it gets stuck in an endless loop. The only solution appears to be to put a try-catch block around the top-level XmlSerializer.Deserialize call (as shown in the test).

Does anyone know why the XmlSerializer is behaving in this way? Ideally I would like to try to catch the exception where it is thrown, rather than having a top-level exception handler, so there is a secondary question as to why the code gets stuck in an endless loop if a try..catch block is added into the collection class.

Here is the exception that is thrown:

System.InvalidOperationException: There is an error in XML document (13, 10). ---> System.InvalidOperationException: There is an error in XML document (13, 10). ---> System.InvalidOperationException: <UnknownElement xmlns='example'> was not expected.
   at Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationReaderGroup.Read1_Group()
   --- End of inner exception stack trace ---
   at System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader, String encodingStyle, XmlDeserializationEvents events)
   at System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader)
   at XmlSerializerTest.EntityCollection~1.ReadXmlElements(XmlReader reader) in C:\source\repos\XmlSerializerTest\XmlSerializerTest\EntityCollection.cs:line 55
   at XmlSerializerTest.EntityCollection~1.ReadXml(XmlReader reader) in C:\Users\NGGMN9O\source\repos\XmlSerializerTest\XmlSerializerTest\EntityCollection.cs:line 41
   at System.Xml.Serialization.XmlSerializationReader.ReadSerializable(IXmlSerializable serializable, Boolean wrappedAny)
   at Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationReaderExample.Read2_Example(Boolean isNullable, Boolean checkType)
   at Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationReaderExample.Read3_Example()
   --- End of inner exception stack trace ---
   at System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader, String encodingStyle, XmlDeserializationEvents events)
   at System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader)
   at XmlSerializerTest.StackOverflowExample.InvalidElementInGroupTest() in C:\source\repos\XmlSerializerTest\XmlSerializerTest\XmlSerializerTest.cs:line 35

Schema.xsd

<?xml version="1.0" encoding="utf-8" ?>
<xs:schema xmlns:local="example"
           attributeFormDefault="unqualified"
           elementFormDefault="qualified"
           targetNamespace="example"
           version="1.0"
           xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <!--  Attribute Groups -->
  <xs:attributeGroup name="Identifiers">
    <xs:attribute name="Id"
                  type="xs:string"
                  use="required" />
    <xs:attribute name="Name"
                  type="xs:string"
                  use="required" />
  </xs:attributeGroup>
  <!-- Complex Types -->
  <xs:complexType abstract="true"
                  name="Entity">
    <xs:sequence>
      <xs:element name="Description"
                  type="xs:string"
                  minOccurs="0"
                  maxOccurs="1" />
    </xs:sequence>
    <xs:attributeGroup ref="local:Identifiers" />
  </xs:complexType>
  <xs:complexType name="DerivedEntity">
    <xs:complexContent>
      <xs:extension base="local:Entity">
        <xs:attribute name="Parameter"
                      use="required" />
      </xs:extension>
    </xs:complexContent>
  </xs:complexType>
  <xs:complexType name="Groups">
      <xs:sequence>
          <xs:element name="Group" type="local:Group" minOccurs="0" maxOccurs="unbounded"/>
      </xs:sequence>
  </xs:complexType>
  <xs:complexType name="Group">
    <xs:complexContent>
      <xs:extension base="local:Entity">
        <xs:sequence>
          <xs:element name="DerivedEntity"
                      type="local:DerivedEntity"
                      minOccurs="0"
                      maxOccurs="unbounded" />
        </xs:sequence>
      </xs:extension>
    </xs:complexContent>
  </xs:complexType>
  <!-- Main Schema Definition -->
  <xs:element name="Example">
      <xs:complexType>
          <xs:sequence>
              <xs:element name="Groups"
                          type="local:Groups"
                          minOccurs="1"
                          maxOccurs="1" />
          </xs:sequence>
      </xs:complexType>
  </xs:element>
</xs:schema>

InvalidElementInGroup.xml

<?xml version="1.0"?>
<Example xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="example">
    <Groups>
        <Group Name="abc" Id="123">
            <DerivedEntity Id="123" Name="xyz" Parameter="ijk">
                <Description>def</Description>
            </DerivedEntity>
            <DerivedEntity Id="234" Name="bob" Parameter="12"/>
        </Group>
        <Group Name="def" Id="124">
            <Description>This is a description.</Description>
        </Group>
        <UnknownElement/>
    </Groups>
</Example>

The Implementation Note: The code shown in this example is not the production code. I know that I could just use a List<T> implementation which supports serialization without needing to implement IXmlSerializable.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;

namespace XmlSerializerTest
{
    public class Example
    {
        public Example()
        {
            Groups = new Groups();
        }

        public Groups Groups { get; set; }
    }

    public class Groups : EntityCollection<Group>
    {

    }
    public class Group : Entity, IXmlSerializable
    {
        private EntityCollection<DerivedEntity> entityCollection;

        public Group()
        {
            this.entityCollection = new EntityCollection<DerivedEntity>();
        }

        #region IXmlSerializable Implementation

        public XmlSchema GetSchema()
        {
            return null;
        }

        public void ReadXml(XmlReader reader)
        {
            reader.MoveToContent();

            // Read the attributes
            ReadXmlAttributes(reader);

            // Consume the start element
            bool isEmptyElement = reader.IsEmptyElement;
            reader.ReadStartElement();
            if (!isEmptyElement)
            {
                ReadXmlElements(reader);
                reader.ReadEndElement();
            }
        }

        /// <summary>
        /// Reads the XML elements.
        /// </summary>
        /// <param name="reader">The reader.</param>
        public override void ReadXmlElements(XmlReader reader)
        {
            // Handle the optional base class description element
            base.ReadXmlElements(reader);

            entityCollection.ReadXmlElements(reader);
        }

        public void WriteXml(XmlWriter writer)
        {
            throw new NotImplementedException();
        }

        #endregion
    }

    public class EntityCollection<T> : IXmlSerializable, IList<T> where T : Entity
    {
        private List<T> childEntityField;

        public EntityCollection()
        {
            childEntityField = new List<T>();
        }

        #region IXmlSerializable Implementation

        public XmlSchema GetSchema()
        {
            return null;
        }

        public void ReadXml(XmlReader reader)
        {
            reader.MoveToContent();

            // Read the attributes
            ReadXmlAttributes(reader);

            // Consume the start element
            bool isEmptyElement = reader.IsEmptyElement;
            reader.ReadStartElement();
            if (!isEmptyElement)
            {
                ReadXmlElements(reader);
                reader.ReadEndElement();
            }
        }

        public virtual void ReadXmlAttributes(XmlReader reader)
        {
        }

        public virtual void ReadXmlElements(XmlReader reader)
        {
            XmlSerializer deserializer = new XmlSerializer(typeof(T), "example");
            while (reader.IsStartElement())
            {
                T item = (T)deserializer.Deserialize(reader);  // throws an InvalidOperationException if an unknown element is encountered.
                if (item != null)
                {
                    Add(item);
                }
            }
        }

        public void WriteXml(XmlWriter writer)
        {
            throw new NotImplementedException();
        }
        #endregion

        #region IList Implementation

        public IEnumerator<T> GetEnumerator()
        {
            return childEntityField.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return ((IEnumerable)childEntityField).GetEnumerator();
        }

        public void Add(T item)
        {
            childEntityField.Add(item);
        }

        public void Clear()
        {
            childEntityField.Clear();
        }

        public bool Contains(T item)
        {
            return childEntityField.Contains(item);
        }

        public void CopyTo(T[] array, int arrayIndex)
        {
            childEntityField.CopyTo(array, arrayIndex);
        }

        public bool Remove(T item)
        {
            return childEntityField.Remove(item);
        }

        public int Count => childEntityField.Count;

        public bool IsReadOnly => ((ICollection<T>)childEntityField).IsReadOnly;

        public int IndexOf(T item)
        {
            return childEntityField.IndexOf(item);
        }

        public void Insert(int index, T item)
        {
            childEntityField.Insert(index, item);
        }

        public void RemoveAt(int index)
        {
            childEntityField.RemoveAt(index);
        }

        public T this[int index]
        {
            get => childEntityField[index];
            set => childEntityField[index] = value;
        }

        #endregion
    }

    [System.Xml.Serialization.XmlIncludeAttribute(typeof(DerivedEntity))]
    public abstract class Entity
    {

        public string Description { get; set; }

        public string Id { get; set; }

        public string Name { get; set; }

        public virtual void ReadXmlAttributes(XmlReader reader)
        {
            Id = reader.GetAttribute("Id");
            Name = reader.GetAttribute("Name");
        }

        public virtual void ReadXmlElements(XmlReader reader)
        {
            if (reader.IsStartElement("Description"))
            {
                Description = reader.ReadElementContentAsString();
            }
        }
    }

    public class DerivedEntity : Entity
    {
        public string Parameter { get; set; }
    }
}

The Test

namespace XmlSerializerTest
{
    using System;
    using System.IO;
    using System.Xml;
    using System.Xml.Schema;
    using System.Xml.Serialization;
    using Microsoft.VisualStudio.TestTools.UnitTesting;

    [TestClass]
    public class StackOverflowExample
    {
        [TestMethod]
        [DeploymentItem(@"Schema.xsd")]
        [DeploymentItem(@"InvalidElementInGroup.xml")]
        public void InvalidElementInGroupTest()
        {
            // Open the file
            FileStream stream = new FileStream("InvalidElementInGroup.xml", FileMode.Open);

            // Configure settings
            XmlReaderSettings settings = new XmlReaderSettings();
            settings.Schemas.Add(null, @"Schema.xsd");
            settings.ValidationType = ValidationType.Schema;
            settings.ValidationEventHandler += OnValidationEvent;

            XmlSerializer xmlDeserializer = new XmlSerializer(typeof(Example), "example");

            // Deserialize from the stream
            stream.Position = 0;
            XmlReader xmlReader = XmlReader.Create(stream, settings);

            try
            {
                Example deserializedObject = (Example)xmlDeserializer.Deserialize(xmlReader);
            }
            catch (Exception e)
            {
                Console.WriteLine("Exception: " + e);
            }
        }

        private void OnValidationEvent(object sender, ValidationEventArgs e)
        {
            Console.WriteLine("Validation Event: " + e.Message);
        }
    }
}

解决方案

Your basic problem is that you have an abstract base class Entity whose inheritors sometimes implement IXmlSerializable and sometimes don't, and when they do they are included in a collection that also implements IXmlSerializable and mingles collection properties with collection children within its XML. Somewhere in the process of reading this XML in you don't advance your XmlReader correctly and deserialization fails.

When implementing IXmlSerializable you need to adhere to the rules stated in this answer to Proper way to implement IXmlSerializable? by Marc Gravell as well as the documentation:

For IXmlSerializable.WriteXml(XmlWriter):

The WriteXml implementation you provide should write out the XML representation of the object. The framework writes a wrapper element and positions the XML writer after its start. Your implementation may write its contents, including child elements. The framework then closes the wrapper element.

For IXmlSerializable.ReadXml(XmlReader):

The ReadXml method must reconstitute your object using the information that was written by the WriteXml method.

When this method is called, the reader is positioned on the start tag that wraps the information for your type. That is, directly on the start tag that indicates the beginning of a serialized object. When this method returns, it must have read the entire element from beginning to end, including all of its contents. Unlike the WriteXml method, the framework does not handle the wrapper element automatically. Your implementation must do so. Failing to observe these positioning rules may cause code to generate unexpected runtime exceptions or corrupt data.

Notice specifically that ReadXml() must entirely consume the container element. This turns out to be problematic in inheritance scenarios; is the base class responsible for consuming the outer element or the derived class? Furthermore, if some derived class improperly positions the XmlReader during reading, this may pass unnoticed by unit tests but cause subsequent data in the XML file to be ignored or corrupted in production.

Thus it makes sense to create an extension framework for reading and writing IXmlSerializable objects whose base and derived classes all have custom (de)serialization logic, in which the processing of the container element, each attribute, and each child element is separated:

public static class XmlSerializationExtensions
{
    public static void ReadIXmlSerializable(XmlReader reader, Func<XmlReader, bool> handleXmlAttribute, Func<XmlReader, bool> handleXmlElement, Func<XmlReader, bool> handleXmlText)
    {
        //https://docs.microsoft.com/en-us/dotnet/api/system.xml.serialization.ixmlserializable.readxml?view=netframework-4.8#remarks
        //When this method is called, the reader is positioned on the start tag that wraps the information for your type. 
        //That is, directly on the start tag that indicates the beginning of a serialized object. 
        //When this method returns, it must have read the entire element from beginning to end, including all of its contents. 
        //Unlike the WriteXml method, the framework does not handle the wrapper element automatically. Your implementation must do so. 
        //Failing to observe these positioning rules may cause code to generate unexpected runtime exceptions or corrupt data.
        reader.MoveToContent();
        if (reader.NodeType != XmlNodeType.Element)
            throw new XmlException(string.Format("Invalid NodeType {0}", reader.NodeType));
        if (reader.HasAttributes)
        {
            for (int i = 0; i < reader.AttributeCount; i++)
            {
                reader.MoveToAttribute(i);
                handleXmlAttribute(reader);
            }
            reader.MoveToElement(); // Moves the reader back to the element node.
        }
        if (reader.IsEmptyElement)
        {
            reader.Read();
            return;
        }
        reader.ReadStartElement(); // Advance to the first sub element of the wrapper element.
        while (reader.NodeType != XmlNodeType.EndElement)
        {
            if (reader.NodeType == XmlNodeType.Element)
            {
                using (var subReader = reader.ReadSubtree())
                {
                    subReader.MoveToContent();
                    handleXmlElement(subReader);
                }
                // ReadSubtree() leaves the reader positioned ON the end of the element, so read that also.
                reader.Read();
            }
            else if (reader.NodeType == XmlNodeType.Text || reader.NodeType == XmlNodeType.CDATA)
            {
                var type = reader.NodeType;
                handleXmlText(reader);
                // Ensure that the reader was not advanced.
                if (reader.NodeType != type)
                    throw new XmlException(string.Format("handleXmlText incorrectly advanced the reader to a new node {0}", reader.NodeType));
                reader.Read();
            }
            else // Whitespace, comment
            {
                // Skip() leaves the reader positioned AFTER the end of the node.
                reader.Skip();
            }
        }
        // Move past the end of the wrapper element
        reader.ReadEndElement();
    }

    public static void WriteIXmlSerializable(XmlWriter writer, Action<XmlWriter> writeAttributes, Action<XmlWriter> writeNodes)
    {
        //https://docs.microsoft.com/en-us/dotnet/api/system.xml.serialization.ixmlserializable.writexml?view=netframework-4.8#remarks
        //The WriteXml implementation you provide should write out the XML representation of the object. 
        //The framework writes a wrapper element and positions the XML writer after its start. Your implementation may write its contents, including child elements. 
        //The framework then closes the wrapper element.
        writeAttributes(writer);
        writeNodes(writer);
    }
}

Then, modify your data model as follows:

public class Constants
{
    public const string ExampleNamespace = "example";
}

[XmlRoot(Namespace = Constants.ExampleNamespace)]
public class Example
{
    public Example()
    {
        Groups = new Groups();
    }

    public Groups Groups { get; set; }
}

public class Groups : EntityCollection<Group>
{

}

public class EntityCollection<T> : IXmlSerializable, IList<T> where T : Entity
{
    private List<T> childEntityField;

    public EntityCollection()
    {
        childEntityField = new List<T>();
    }

    #region IXmlSerializable Implementation

    public XmlSchema GetSchema() { return null; }

    protected internal virtual bool HandleXmlAttribute(XmlReader reader) { return false; }

    protected internal virtual void WriteAttributes(XmlWriter writer) { }

    protected internal virtual bool HandleXmlElement(XmlReader reader)
    {
        var serializer = new XmlSerializer(typeof(T), Constants.ExampleNamespace);
        if (serializer.CanDeserialize(reader))
        {
            T item = (T)serializer.Deserialize(reader);
            if (item != null)
                Add(item);
            return true;
        }
        return false;
    }

    protected internal virtual void WriteNodes(XmlWriter writer)
    {
        var serializer = new XmlSerializer(typeof(T), Constants.ExampleNamespace);
        foreach (var item in this)
        {
            serializer.Serialize(writer, item);
        }
    }

    public void ReadXml(XmlReader reader)
    {
        XmlSerializationExtensions.ReadIXmlSerializable(reader, r => HandleXmlAttribute(r), r => HandleXmlElement(r), r => false);
    }

    public void WriteXml(XmlWriter writer)
    {
        XmlSerializationExtensions.WriteIXmlSerializable(writer, w => WriteAttributes(w), w => WriteNodes(w));
    }

    #endregion

    #region IList Implementation

    public IEnumerator<T> GetEnumerator()
    {
        return childEntityField.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return ((IEnumerable)childEntityField).GetEnumerator();
    }

    public void Add(T item)
    {
        childEntityField.Add(item);
    }

    public void Clear()
    {
        childEntityField.Clear();
    }

    public bool Contains(T item)
    {
        return childEntityField.Contains(item);
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
        childEntityField.CopyTo(array, arrayIndex);
    }

    public bool Remove(T item)
    {
        return childEntityField.Remove(item);
    }

    public int Count { get { return childEntityField.Count; } }

    public bool IsReadOnly { get { return ((ICollection<T>)childEntityField).IsReadOnly; } }

    public int IndexOf(T item)
    {
        return childEntityField.IndexOf(item);
    }

    public void Insert(int index, T item)
    {
        childEntityField.Insert(index, item);
    }

    public void RemoveAt(int index)
    {
        childEntityField.RemoveAt(index);
    }

    public T this[int index]
    {
        get { return childEntityField[index]; }
        set { childEntityField[index] = value; }
    }

    #endregion
}

public class Group : Entity, IXmlSerializable
{
    private EntityCollection<DerivedEntity> entityCollection;

    public Group()
    {
        this.entityCollection = new EntityCollection<DerivedEntity>();
    }

    #region IXmlSerializable Implementation

    public XmlSchema GetSchema()
    {
        return null;
    }

    protected override bool HandleXmlElement(XmlReader reader)
    {
        if (base.HandleXmlElement(reader))
            return true;
        return entityCollection.HandleXmlElement(reader);
    }

    protected override void WriteNodes(XmlWriter writer)
    {
        base.WriteNodes(writer);
        entityCollection.WriteNodes(writer);
    }

    protected override bool HandleXmlAttribute(XmlReader reader)
    {
        if (base.HandleXmlAttribute(reader))
            return true;
        if (entityCollection.HandleXmlAttribute(reader))
            return true;
        return false;
    }

    protected override void WriteAttributes(XmlWriter writer)
    {
        base.WriteAttributes(writer);
        entityCollection.WriteAttributes(writer);
    }

    public void ReadXml(XmlReader reader)
    {
        XmlSerializationExtensions.ReadIXmlSerializable(reader, r => HandleXmlAttribute(r), r => HandleXmlElement(r), r => false);
    }

    public void WriteXml(XmlWriter writer)
    {
        XmlSerializationExtensions.WriteIXmlSerializable(writer, w => WriteAttributes(w), w => WriteNodes(w));
    }

    #endregion
}

public class DerivedEntity : Entity
{
    [XmlAttribute]
    public string Parameter { get; set; }
}

[System.Xml.Serialization.XmlIncludeAttribute(typeof(DerivedEntity))]
public abstract class Entity
{
    [XmlElement]
    public string Description { get; set; }

    [XmlAttribute]
    public string Id { get; set; }

    [XmlAttribute]
    public string Name { get; set; }

    protected virtual void WriteAttributes(XmlWriter writer)
    {
        if (Id != null)
            writer.WriteAttributeString("Id", Id);
        if (Name != null)
            writer.WriteAttributeString("Name", Name);
    }

    protected virtual bool HandleXmlAttribute(XmlReader reader)
    {
        if (reader.LocalName == "Id")
        {
            Id = reader.Value;
            return true;
        }
        else if (reader.LocalName == "Name")
        {
            Name = reader.Value;
            return true;
        }
        return false;
    }

    protected virtual void WriteNodes(XmlWriter writer)
    {
        if (Description != null)
        {
            writer.WriteElementString("Description", Description);
        }
    }

    protected virtual bool HandleXmlElement(XmlReader reader)
    {
        if (reader.LocalName == "Description")
        {
            Description = reader.ReadElementContentAsString();
            return true;
        }
        return false;
    }
}

And you will be able to deserialize and re-serialize Example successfully. Demo fiddle here.

Notes:

  • Seriously consider simplifying this architecture. This is all too complicated.

  • A single validation event will be correctly raised for <UnknownElement/> inside <Groups>, as no such element appears in the schema.

  • XmlSerializer.Deserialize() will throw an InvalidOperationException when the root XML element name and namespace do not match the expected name and namespace. You can check whether the name and namespace are correct by calling XmlSerializer.CanDeserialize(XmlReader).

  • Be sure to test deserialization of XML with and without indentation. Sometimes a ReadXml() method will advance the reader one node too far, but if the XML contains insignificant indentation (i.e. formatting) then no harm will be done as only an insignificant whitespace node gets skipped.

  • When Entity.HandleXmlElement(XmlReader reader) is overridden in a derived class, the base class method should be called first. If the base class method handles the element, true is returned and the derived class should not try to handle it. Similarly, if the derived class handles the element, true should be returned to more derived classes indicating the element was handled. false is returned when neither the class nor the base class could handle the element.

  • XmlReader.ReadSubtree() can be used to ensure that some derived class cannot misposition the XmlReader inside HandleXmlElement(XmlReader reader).

  • If you use any constructor other than new XmlSerializer(Type) and new XmlSerializer(Type, String) to construct an XmlSerializer, you must construct it only once and cache it statically to avoid a severe memory leak. For why, see the documentation and Memory Leak using StreamReader and XmlSerializer. You are not constructing a serializer in such a manner in your sample code but may be doing so in your production code.

这篇关于当 IXmlSerializable.ReadXml() 内发生架构验证错误时,为什么 XmlSerializer 会抛出异常并引发 ValidationEvent的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆