【问题标题】:XmlSerializer aborts reading after reader.Read()XmlSerializer 在 reader.Read() 之后中止读取
【发布时间】:2018-10-07 10:07:52
【问题描述】:

为什么 XmlSerializer 在使用他的阅读器时会中止阅读?

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

当使用XmlSerializer 时,该类可以被序列化和反序列化。

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 中,我有一个自定义类,我从 XmlSerializer 获取 XmlReader 以自定义方式反序列化。这是通过在我的类中实现接口IXmlSerializable 来完成的。

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() 的这一行。它在文件中间的调试窗口中显示?XmlTextReader(reader).LineNumber,而不是EOF。如果reader.Read()正在使用中(未注释掉),它将读取一个项目而不是下一个项目。

System.Xml.Serialization.XmlSerializerIXmlSerializable结合使用有什么要注意的吗?

【问题讨论】:

标签: c# .net xml-serialization xmlserializer


【解决方案1】:

下载您的项目后,我能够在此处创建接近 Minimal, Complete and Verifiable example 的内容:https://dotnetfiddle.net/OvPQ6J。虽然没有引发异常,但会跳过大量 XML 文件,导致 &lt;ChildItems&gt; 集合缺少条目。

问题是您的ReadXml() 没有按照documentation 中的要求将XmlReader 推进到相应元素的末尾(添加了重点):

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

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

因此,意外的随机异常是错误实现ReadXml() 的记录可能结果。如需更多信息,请参阅 Proper way to implement IXmlSerializable?How to Implement IXmlSerializable Correctly

由于很容易犯这个错误,您可以通过在ReadSubtree() 调用中将XML 读取逻辑括起来或使用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() 的返回可能取决于语言环境。请改用XmlConvert.ToString(Int32) 或只使用XmlWriter.WriteValue(Int32)

  • XmlReader.ReadSubtree()XmlReader留在正在读取元素的EndElement节点上,而XNode.ReadFrom()将阅读器留在紧随其后 正在读取的元素的EndElement 节点。这说明了在StreamingXmlSerializableBase.ReadXml() 中对Read() 的额外调用。

  • 使用XmlReader 手动读取 XML 的代码应始终使用格式化和未格式化的 XML 进行单元测试,因为某些错误只会出现其中之一。 (有关此类示例,请参见 this answerthis one also。)

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

【讨论】:

  • 感谢您付出的所有努力。我看到了,主要变化只是使用XNode.ReadFrom(reader) 而不是read.Read()。在这种情况下,读者在离开ReadXml(XmlReader reader) 时会指向Child 中的NodeType = EndElement。通过使用reader.Read() 11 次手动移动到此标签,它具有相同的效果并且可以正常工作。即使您的课程结构更好。干得好!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-11
相关资源
最近更新 更多