简体   繁体   中英

XmlSerializer aborts reading after reader.Read()

Why the XmlSerializer aborts reading when using his reader? 在此处输入图片说明

Sample XML - here - .
Download sample project - here - .

When using the XmlSerializer the class can be serialized and deserialized.

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

Model to serialize

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

In the ConnectionModel I have one custom class where I grab the XmlReader from XmlSerializer to deserialize on custom way. This is done by implementing interface IXmlSerializable to my 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() will not throw any exception at all. And it reads the values correctly into the model but for testing, I commented all out, except this one line of reader.Read() . It shows ?XmlTextReader(reader).LineNumber in debug Window with middle of the file, not EOF. If reader.Read() is in use (not commented out), it will read one single item instead of the next items.

Is there anything to focus on when using System.Xml.Serialization.XmlSerializer when using IXmlSerializable combined?

After downloading your project, I was able to create something approaching a Minimal, Complete and Verifiable example here: https://dotnetfiddle.net/OvPQ6J . While no exception is thrown, large chunks of the XML file are skipped causing the <ChildItems> collection to be missing entries.

The problem is that your ReadXml() is not advancing the XmlReader past the end of the corresponding element as required in the documentation (emphasis added):

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.

Thus unexpected, random exceptions are a documented possible consequence of incorrectly implementing ReadXml() . For more, see Proper way to implement IXmlSerializable? and How to Implement IXmlSerializable Correctly .

Since it is so easy to make this mistake, you can systematically avoid it by either bracketing your XML reading logic in a call to ReadSubtree() or loading the entire XML into memory with XNode.ReadFrom() , eg using one of the following two base classes:

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);
}

And here's a fixed version of your SerializableClass which uses 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();
    }
}

Notes:

  • In your original code you write the integer Number to the XML as a string using ToString() :

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

    This can cause problems as the return of ToString() can be locale-dependent. Instead use XmlConvert.ToString(Int32) or just XmlWriter.WriteValue(Int32) .

  • XmlReader.ReadSubtree() leaves the XmlReader positioned on the EndElement node of the element being read, while XNode.ReadFrom() leaves the reader positioned immediately after the EndElement node of the element being read. This accounts for the extra call to Read() in StreamingXmlSerializableBase.ReadXml() .

  • Code that manually reads XML using XmlReader should always be unit-tested with both formatted and unformatted XML, because certain bugs will only arise with one or the other. (See eg this answer and this one also for examples of such.)

Sample working .Net fiddle here: https://dotnetfiddle.net/s9OJOQ .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM