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()
, e.g. 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 e.g. this answer and this one also for examples of such.)
Sample working .Net fiddle here: https://dotnetfiddle.net/s9OJOQ.