XmlSerializer 在 reader.Read() 后中止读取

XmlSerializer aborts reading after reader.Read()

为什么 XmlSerializer 在使用 reader 时中止读取?

样本 XML - here -
下载示例项目- here -

使用XmlSerializer时,class可以序列化和反序列化。

var serializer = new XmlSerializer(typeof(MainItem));
using (var reader = new StreamReader(SettingFile.FullName))
{
    var deserializedObject = serializer.Deserialize(reader);
    ret = (MainItem)deserializedObject;
}

要序列化的模型

public class MainItem
{
    public List<Child1> Child1{ get; set; }
}

ConnectionModel 中,我有一个自定义 class,我从 XmlSerializer 中获取 XmlReader,以自定义方式反序列化。这是通过将接口 IXmlSerializable 实现到我的 class.

来完成的
public XmlSchema GetSchema()
{
    return null;
}


public void ReadXml(XmlReader reader)
{

    reader.Read(); // <-- as soon as I comment this out, the serializer will finish proper!
}



public void WriteXml(XmlWriter writer)
{
    writer.WriteStartElement(nameof(PropertyInfo.Name));
    writer.WriteString(Value.Name);
    writer.WriteEndElement();

    writer.WriteStartElement(nameof(PropertyInfo.PropertyType));
    writer.WriteString(Value.PropertyType.Name);
    writer.WriteEndElement();

    writer.WriteStartElement(XML_PropertyTypeFullName);
    writer.WriteString(Value.PropertyType.FullName);
    writer.WriteEndElement();

    writer.WriteStartElement(nameof(PropertyInfo.CanRead));
    writer.WriteString(Value.CanRead.ToString());
    writer.WriteEndElement();

    writer.WriteStartElement(nameof(PropertyInfo.CanWrite));
    writer.WriteString(Value.CanWrite.ToString());
    writer.WriteEndElement();

}

reader.Read() 不会抛出任何异常。它会将值正确读入模型,但为了测试,我全部注释掉了,除了这一行 reader.Read()。它在文件中间显示调试 Window 中的 ?XmlTextReader(reader).LineNumber,而不是 EOF。如果 reader.Read() 正在使用(未注释掉),它将读取一个项目而不是下一个项目。

结合使用IXmlSerializable时使用System.Xml.Serialization.XmlSerializer有什么需要注意的地方吗?

下载您的项目后,我能够创建接近 Minimal, Complete and Verifiable example here: https://dotnetfiddle.net/OvPQ6J 的东西。虽然没有抛出异常,但跳过了 XML 文件的大块,导致 <ChildItems> 集合缺少条目。

问题是您的 ReadXml() 没有按照 documentation 中的要求将 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() 的可能后果。有关详细信息,请参阅 Proper way to implement IXmlSerializable? and How to Implement IXmlSerializable Correctly.

由于很容易犯这个错误,您可以通过在 ReadSubtree() or loading the entire XML into memory with XNode.ReadFrom() 调用中括起 XML 读取逻辑来系统地避免它,例如使用以下两个基数之一 类:

public abstract class StreamingXmlSerializableBase : IXmlSerializable
{
    // Populate the object with the XmlReader returned by ReadSubtree
    protected abstract void Populate(XmlReader reader);

    public XmlSchema GetSchema() => null;

    public void ReadXml(XmlReader reader)
    {
        reader.MoveToContent();
        // Consume all child nodes of the current element using ReadSubtree()
        using (var subReader = reader.ReadSubtree())
        {
            subReader.MoveToContent();
            Populate(subReader);
        }
        reader.Read(); // Consume the end element itself.
    }   

    public abstract void WriteXml(XmlWriter writer);
}

public abstract class XmlSerializableBase : IXmlSerializable
{
    // Populate the object with an XElement loaded from the XmlReader for the current node
    protected abstract void Populate(XElement element);

    public XmlSchema GetSchema() => null;

    public void ReadXml(XmlReader reader)
    {
        reader.MoveToContent();
        var element = (XElement)XNode.ReadFrom(reader);
        Populate(element);
    }   

    public abstract void WriteXml(XmlWriter writer);
}

这是您的 SerializableClass 的固定版本,它使用 XmlSerializableBase:

public class SerializableClass : XmlSerializableBase
{
    public string Title { get; set; } = "Test title";
    public string Description { get; set; } = "Super description";
    public int Number { get; set; } = (int)(DateTime.Now.Ticks % 99);

    protected override void Populate(XElement element)
    {
        this.Title = (string)element.Element(nameof(this.Title));
        this.Description = (string)element.Element(nameof(this.Description));
        // Leave Number unchanged if not present in the XML
        this.Number = (int?)element.Element(nameof(this.Number)) ?? this.Number;  
    }

    public override void WriteXml(XmlWriter writer)
    {
        writer.WriteStartElement(nameof(this.Title));
        writer.WriteString(this.Title);
        writer.WriteEndElement();

        writer.WriteStartElement(nameof(this.Description));
        writer.WriteString(this.Description);
        writer.WriteEndElement();

        writer.WriteStartElement(nameof(this.Number));
        // Do not use ToString() as it is locale-dependent.  
        // Instead use XmlConvert.ToString(), or just writer.WriteValue
        writer.WriteValue(this.Number);
        writer.WriteEndElement();
    }
}

备注:

  • 在您的原始代码中,您使用 ToString() 将整数 Number 作为字符串写入 XML:

    writer.WriteString(this.Number.ToString());  
    

    这可能会导致问题,因为 ToString() 的 return 可能取决于区域设置。而是使用 XmlConvert.ToString(Int32) or just XmlWriter.WriteValue(Int32).

  • XmlReader.ReadSubtree() 使 XmlReader 位于 正在读取的元素的 EndElement 节点上,而 XNode.ReadFrom() 将 reader 的位置 紧跟在 正在读取的元素的 EndElement 节点之后。这说明了在 StreamingXmlSerializableBase.ReadXml().

  • 中对 Read() 的额外调用
  • 使用 XmlReader 手动读取 XML 的代码应始终使用格式化和未格式化 XML 进行单元测试,因为某些错误只会出现在一个或另一个。 (有关此类示例,请参见 this answer and this one also。)

工作 .Net fiddle 示例:https://dotnetfiddle.net/s9OJOQ.