Search code examples
c#xmlserializationixmlserializable

Dictionary<string, string> serialization: after ReadXml(), subsequent XML elements are skipped


In one of my objects I have a dictionary that is serialized by implementing IXmlSerializable:

    public SerializableStringDictionary Parameters { get; set; }

When I serialize a list of these object with the normal .NET serializer it serializes fine, however deserialization is only doing a single object - elements in the XML file that follow the serializable dictionary are getting skipped.

For example, I have a list of <MaintenanceIndicators>. I show only one below, but this is a List<MaintenanceIndicator> in the code. The serialization of a list with multiple entries works fine but deserializing multiple only gives me 1 MaintenanceIndicator in the List. Its parameters property however is deserialized ok. No exceptions are thrown in the code.

I use the following code for deserialization:

    public void ReadXml(XmlReader reader)
    {
        bool wasEmpty = reader.IsEmptyElement;
        // jump to <parameters>
        reader.Read();

        if (wasEmpty)
            return;

        // read until we reach the last element
        while (reader.NodeType != System.Xml.XmlNodeType.EndElement)
        {
            // jump to <item>
            reader.MoveToContent();

            // jump to key attribute and read
            reader.MoveToAttribute("key");
            string key = reader.GetAttribute("key");

            // jump to value attribute and read
            reader.MoveToAttribute("value");
            string value = reader.GetAttribute("value");

            // add item to the dictionary
            this.Add(key, value);

            // jump to next <item>
            reader.ReadStartElement("item");
            reader.MoveToContent(); // workaround to trigger node type
        }
    }

My structure in XML looks as follows:

<MaintenanceIndicators>
<MaintenanceIndicator Id="1" Name="Test" Description="Test" Type="LimitMonitor" ExecutionOrder="1">
  <Parameters>
    <item key="Input" value="a" />
    <item key="Output" value="b" />
    <item key="Direction" value="GreaterThan" />
    <item key="LimitValue" value="1" />
    <item key="Hysteresis" value="1" />
  </Parameters>
</MaintenanceIndicator>
 <!-- Any subsequent MaintenanceIndicator indicator element will not be read -->
</MaintenanceIndicators>

Solution

  • Your question is doesn't contain a complete example of your problem. To confirm, you have the following (simplified) class definitions, with SerializableStringDictionary as shown in your question:

    public class MaintenanceIndicator
    {
        public MaintenanceIndicator()
        {
            this.Parameters = new SerializableStringDictionary();
        }
        public SerializableStringDictionary Parameters { get; set; }
    }
    
    [XmlRoot("MaintenanceIndicators")]
    public class MaintenanceIndicators
    {
        [XmlElement("MaintenanceIndicator")]
        public List<MaintenanceIndicator> MaintenanceIndicatorList { get; set; }
    }
    

    And the following XML:

    <MaintenanceIndicators>
      <MaintenanceIndicator>
        <Parameters>
          <item key="Input" value="a" />
          <item key="Output" value="b" />
        </Parameters>
      </MaintenanceIndicator>
      <MaintenanceIndicator>
        <Parameters>
          <item key="Input2" value="a" />
          <item key="Output2" value="b" />
        </Parameters>
      </MaintenanceIndicator>
    </MaintenanceIndicators>
    

    In this case, using your ReadXml(), I was able to reproduce that, when reading the parsing above, only the first <MaintenanceIndicator> element was deserialized.

    Your problem is that, in ReadXml(), you do not consume the </Parameters> end node. From the docs:

    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.

    Failing to call reader.Read(); at the end of ReadXml() to read the element end will cause all subsequent elements in the XML to be skipped or otherwise read wrongly.

    Thus you should modify ReadXml() as follows:

        public void ReadXml(XmlReader reader)
        {
            bool wasEmpty = reader.IsEmptyElement;
            // jump to <parameters>
            reader.Read();
    
            if (wasEmpty)
                return;
    
            // read until we reach the last element
            while (reader.NodeType != System.Xml.XmlNodeType.EndElement)
            {
                // jump to <item>
                reader.MoveToContent();
    
                // jump to key attribute and read
                string key = reader.GetAttribute("key");
    
                // jump to value attribute and read
                string value = reader.GetAttribute("value");
    
                // add item to the dictionary
                this.Add(key, value);
    
                // jump to next <item>
                reader.ReadStartElement("item");
                reader.MoveToContent(); // workaround to trigger node type
            }
            // Consume the </Parameters> node as required by the documentation
            // https://msdn.microsoft.com/en-us/library/system.xml.serialization.ixmlserializable.readxml%28v=vs.110%29.aspx
            // Unlike the WriteXml method, the framework does not handle the wrapper element automatically. Your implementation must do so. 
            reader.Read();
        }
    

    Note there is no need to call reader.MoveToAttribute(string name) before calling reader.GetAttribute(string name).