Custom serialization of a dictionary fails when xml has indentation/line break

柔情痞子 提交于 2019-12-10 22:25:33

问题


In order to have a cleaner XML of a Dictionary serialization, I wrote a custom class, that implements IXmlSerializable.

My custom class is defined like this:

public class MyCollection : System.Collections.Generic.Dictionary<string, string>, IXmlSerializable
{
    private const string XmlElementName = "MyData";
    private const string XmlAttributeId = "Id";

    public XmlSchema GetSchema()
    {
        return null;
    }

    public void ReadXml(XmlReader reader)
    {
        reader.MoveToContent();
        while (reader.Read())
        {
            if(reader.LocalName == XmlElementName)
            {
                var tag = reader.GetAttribute(XmlAttributeId);
                var content = reader.ReadElementContentAsString(); 

                this.Add(tag, content);
            }
        }
    }

    public void WriteXml(System.Xml.XmlWriter writer)
    {
        foreach (string key in this.Keys)
        {
            writer.WriteStartElement(XmlElementName);
            writer.WriteAttributeString(XmlAttributeId, key);
            writer.WriteString(this[key]);
            writer.WriteEndElement();
        }
    }
}

My code works with this XML snippet:

<MyCollection xmlns="http://schemas.datacontract.org/2004/07/MyProject" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
    <MyData Id="1">some content</MyData>
    <MyData Id="2">some other content</MyData>
</MyCollection>

However, when I have this minified XML, my code throws an exception:

<MyCollection xmlns="http://schemas.datacontract.org/2004/07/MyProject" xmlns:i="http://www.w3.org/2001/XMLSchema-instance"><MyData Id="1">some content </MyData><MyData Id="2">some other content</MyData></MyCollection>

The exception is:

System.InvalidOperationException: The ReadElementContentAsString method is not supported on node type EndElement

It's thrown on the call to ReadElementContentAsString.

How to fix my code?

I can repro the issue using :

var xml = @"<MyCollection xmlns=""http://schemas.datacontract.org/2004/07/MyProject"" xmlns:i=""http://www.w3.org/2001/XMLSchema-instance""><MyData Id=""1"">some content </MyData><MyData Id=""2"">some other content</MyData></MyCollection>";

var raw = Encoding.UTF8.GetBytes(xml);

var serializer = new DataContractSerializer(typeof(MyCollection));

using (var ms = new MemoryStream(raw))
{
    var result = serializer.ReadObject(ms); // Exception throws here
}

回答1:


Your problem is that reader.ReadElementContentAsString() positions the reader at the beginning of the next node, not the end of the current node. Then, your subsequent unconditional call to reader.Read() consumes that next node. When that node is whitespace no harm is done, but when the node is an element, the element is skipped.

The following version of your MyCollection fixes this problem:

public class MyCollection : System.Collections.Generic.Dictionary<string, string>, IXmlSerializable
{
    public XmlSchema GetSchema()
    {
        return null;
    }

    public void ReadXml(XmlReader reader)
    {
        using (var subReader = reader.ReadSubtree())
        {
            XmlKeyValueListHelper.ReadKeyValueXml(subReader, this);
        }
        // Consume the EndElement also (or move past the current element if reader.IsEmptyElement).
        reader.Read();
    }

    public void WriteXml(System.Xml.XmlWriter writer)
    {
        XmlKeyValueListHelper.WriteKeyValueXml(writer, this);
    }
}

public static class XmlKeyValueListHelper
{
    private const string XmlElementName = "MyData";
    private const string XmlAttributeId = "Id";

    public static void WriteKeyValueXml(System.Xml.XmlWriter writer, ICollection<KeyValuePair<string, string>> collection)
    {
        foreach (var pair in collection)
        {
            writer.WriteStartElement(XmlElementName);
            writer.WriteAttributeString(XmlAttributeId, pair.Key);
            writer.WriteString(pair.Value);
            writer.WriteEndElement();
        }
    }

    public static void ReadKeyValueXml(System.Xml.XmlReader reader, ICollection<KeyValuePair<string, string>> collection)
    {
        if (reader.IsEmptyElement)
        {
            reader.Read();
            return;
        }

        reader.ReadStartElement(); // Advance to the first sub element of the list element.
        while (reader.NodeType != XmlNodeType.EndElement)
        {
            if (reader.NodeType == XmlNodeType.Element && reader.LocalName == XmlElementName)
            {
                var tag = reader.GetAttribute(XmlAttributeId);
                string content;
                if (reader.IsEmptyElement)
                {
                    content = string.Empty;
                    // Move past the end of item element
                    reader.Read();
                }
                else
                {
                    // Read content and move past the end of item element
                    content = reader.ReadElementContentAsString();
                }
                collection.Add(new KeyValuePair<string, string>(tag, content));
            }
            else
            {
                // For instance a comment.
                reader.Skip();
            }
        }
        // Move past the end of the list element
        reader.ReadEndElement();
    }
}

Some notes:

  • By using XmlReader.ReadSubtree() I ensure that ReadXml() does not read past the end of the MyCollection element thus corrupting future elements -- an easy mistake to make when implementing IXmlSerializable.

  • By checking for reader.NodeType == XmlNodeType.Element && reader.LocalName == XmlElementName I ignore unexpected types of nodes such as comments.

Working .Net fiddle.



来源:https://stackoverflow.com/questions/45844241/custom-serialization-of-a-dictionary-fails-when-xml-has-indentation-line-break

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!