为什么 XmlSerializer 在 IXmlSerializable.ReadXml() 内部发生架构验证错误时抛出异常并引发 ValidationEvent
Why does XmlSerializer throws an Exception and raise a ValidationEvent when a schema validation error occurs inside IXmlSerializable.ReadXml()
我已经编写了一些测试来读取 XML 文件并根据 XSD 模式验证它。
我的数据对象混合使用了基于属性的和自定义的 IXmlSerializable 实现,我正在使用 XmlSerializer 执行反序列化。
我的测试涉及将未知元素插入到 XML 中,因此它不符合架构。然后我测试验证事件是否触发。
如果未知元素放置在 XML 中,因此它是基于属性的数据之一的子元素 classes(即属性用 XmlAttribute 和 XmlElement 属性修饰),则验证正确触发。
但是,如果未知元素放置在 XML 中,因此它是 IXmlSerializable class 之一的子元素,则抛出 System.InvalidOperationException,但验证确实还在火
自定义集合的 ReadXmlElements 中的代码创建了一个新的 XmlSerializer 以读取子项,这是抛出 InvalidOperationException 的 Deserialize 调用。
如果我在这个调用周围放置一个 try .. catch 块,它就会陷入无限循环。唯一的解决方案似乎是在顶级 XmlSerializer.Deserialize 调用周围放置一个 try-catch 块(如测试所示)。
有人知道为什么 XmlSerializer 会这样吗?
理想情况下,我想尝试在抛出异常的地方捕获异常,而不是拥有顶级异常处理程序,因此还有一个次要问题,即如果 try..catch 块是,为什么代码会陷入无限循环添加到集合 class.
这是抛出的异常:
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.WhosebugExample.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>
实施
注意:此示例中显示的代码不是生产代码。我知道我可以只使用支持序列化的 List<T>
实现,而无需实现 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; }
}
}
测试
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 WhosebugExample
{
[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);
}
}
}
你的基本问题是你有一个抽象基础 class Entity
其继承者 有时 实现 IXmlSerializable
有时不实现,当它们这样做时,它们将包含在一个集合中,该集合也实现了 IXmlSerializable
并将集合属性与其 XML 中的子集合混合在一起。在阅读此 XML 的过程中,您没有正确推进 XmlReader
并且反序列化失败。
实施 IXmlSerializable
时,您需要遵守 this answer to Proper way to implement IXmlSerializable? by Marc Gravell 中规定的规则以及文档:
对于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.
对于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.
特别注意 ReadXml()
必须完全消耗容器元素。这在继承场景中被证明是有问题的;是基础 class 负责消耗外部元素还是派生的 class?此外,如果某些派生 class 在读取期间不正确地定位 XmlReader
,这可能会被单元测试忽略,但会导致 XML 文件中的后续数据在生产中被忽略或损坏。
因此,创建一个用于读写 IXmlSerializable
对象的扩展框架是有意义的,这些对象的基础和派生 classes 都具有自定义(反)序列化逻辑,其中容器的处理元素,每个属性,每个子元素分开:
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);
}
}
然后,修改你的数据模型如下:
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;
}
}
并且您将能够成功反序列化和重新序列化 Example
。演示 fiddle here.
备注:
认真考虑简化这个架构。这太复杂了。
将为 <Groups>
内的 <UnknownElement/>
正确引发单个验证事件,因为架构中没有出现此类元素。
XmlSerializer.Deserialize()
当根 XML 元素名称和命名空间与预期的名称和命名空间不匹配时,将抛出 InvalidOperationException
。您可以通过调用XmlSerializer.CanDeserialize(XmlReader)
.
来检查名称和命名空间是否正确
一定要测试 XML 的反序列化有无缩进。有时 ReadXml()
方法会将 reader 一个节点推进得太远,但如果 XML 包含无关紧要的缩进(即格式化),则不会造成任何伤害,因为只会跳过一个无关紧要的空白节点.
当 Entity.HandleXmlElement(XmlReader reader)
在派生的 class 中被覆盖时,应该首先调用基础 class 方法。如果基本 class 方法处理该元素,则返回 true
并且派生的 class 不应尝试处理它。类似地,如果派生的 class 处理元素,则 true
应该返回到更多派生的 classes 指示元素已被处理。当 class 和基础 class 都不能处理元素时返回 false
。
XmlReader.ReadSubtree()
可用于确保某些派生的 class 不会错位 HandleXmlElement(XmlReader reader)
.
[= 中的 XmlReader
94=]
如果您使用除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以外的任何构造函数。您没有在示例代码中以这种方式构建序列化程序,但可能在您的生产代码中这样做。
我已经编写了一些测试来读取 XML 文件并根据 XSD 模式验证它。 我的数据对象混合使用了基于属性的和自定义的 IXmlSerializable 实现,我正在使用 XmlSerializer 执行反序列化。
我的测试涉及将未知元素插入到 XML 中,因此它不符合架构。然后我测试验证事件是否触发。
如果未知元素放置在 XML 中,因此它是基于属性的数据之一的子元素 classes(即属性用 XmlAttribute 和 XmlElement 属性修饰),则验证正确触发。
但是,如果未知元素放置在 XML 中,因此它是 IXmlSerializable class 之一的子元素,则抛出 System.InvalidOperationException,但验证确实还在火
自定义集合的 ReadXmlElements 中的代码创建了一个新的 XmlSerializer 以读取子项,这是抛出 InvalidOperationException 的 Deserialize 调用。
如果我在这个调用周围放置一个 try .. catch 块,它就会陷入无限循环。唯一的解决方案似乎是在顶级 XmlSerializer.Deserialize 调用周围放置一个 try-catch 块(如测试所示)。
有人知道为什么 XmlSerializer 会这样吗? 理想情况下,我想尝试在抛出异常的地方捕获异常,而不是拥有顶级异常处理程序,因此还有一个次要问题,即如果 try..catch 块是,为什么代码会陷入无限循环添加到集合 class.
这是抛出的异常:
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.WhosebugExample.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>
实施
注意:此示例中显示的代码不是生产代码。我知道我可以只使用支持序列化的 List<T>
实现,而无需实现 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; }
}
}
测试
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 WhosebugExample
{
[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);
}
}
}
你的基本问题是你有一个抽象基础 class Entity
其继承者 有时 实现 IXmlSerializable
有时不实现,当它们这样做时,它们将包含在一个集合中,该集合也实现了 IXmlSerializable
并将集合属性与其 XML 中的子集合混合在一起。在阅读此 XML 的过程中,您没有正确推进 XmlReader
并且反序列化失败。
实施 IXmlSerializable
时,您需要遵守 this answer to Proper way to implement IXmlSerializable? by Marc Gravell 中规定的规则以及文档:
对于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.
对于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.
特别注意 ReadXml()
必须完全消耗容器元素。这在继承场景中被证明是有问题的;是基础 class 负责消耗外部元素还是派生的 class?此外,如果某些派生 class 在读取期间不正确地定位 XmlReader
,这可能会被单元测试忽略,但会导致 XML 文件中的后续数据在生产中被忽略或损坏。
因此,创建一个用于读写 IXmlSerializable
对象的扩展框架是有意义的,这些对象的基础和派生 classes 都具有自定义(反)序列化逻辑,其中容器的处理元素,每个属性,每个子元素分开:
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);
}
}
然后,修改你的数据模型如下:
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;
}
}
并且您将能够成功反序列化和重新序列化 Example
。演示 fiddle here.
备注:
认真考虑简化这个架构。这太复杂了。
将为
<Groups>
内的<UnknownElement/>
正确引发单个验证事件,因为架构中没有出现此类元素。XmlSerializer.Deserialize()
当根 XML 元素名称和命名空间与预期的名称和命名空间不匹配时,将抛出InvalidOperationException
。您可以通过调用XmlSerializer.CanDeserialize(XmlReader)
. 来检查名称和命名空间是否正确
一定要测试 XML 的反序列化有无缩进。有时
ReadXml()
方法会将 reader 一个节点推进得太远,但如果 XML 包含无关紧要的缩进(即格式化),则不会造成任何伤害,因为只会跳过一个无关紧要的空白节点.当
Entity.HandleXmlElement(XmlReader reader)
在派生的 class 中被覆盖时,应该首先调用基础 class 方法。如果基本 class 方法处理该元素,则返回true
并且派生的 class 不应尝试处理它。类似地,如果派生的 class 处理元素,则true
应该返回到更多派生的 classes 指示元素已被处理。当 class 和基础 class 都不能处理元素时返回false
。
[= 中的XmlReader.ReadSubtree()
可用于确保某些派生的 class 不会错位HandleXmlElement(XmlReader reader)
.XmlReader
94=]如果您使用除
new XmlSerializer(Type)
andnew XmlSerializer(Type, String)
to construct anXmlSerializer
, 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以外的任何构造函数。您没有在示例代码中以这种方式构建序列化程序,但可能在您的生产代码中这样做。