Deserializing collection of types implementing IXmlSerializable runs forever

感情迁移 提交于 2019-12-11 06:31:28

问题


I have a class implementing IXmlSerializable. This class contains a few properties. Serializing and Deserializing a single instance of the class works fine. But in case of collection of the class, Serialization works fine but Deserialization runs forever. Here is a code snippet. I am using .Net 4.6.2.

public class MyClass : IXmlSerializable
{
    public int A { get; set; }
    public int B { get; set; }

    public XmlSchema GetSchema()
    {
        return null;
    }

    public void ReadXml(XmlReader reader)
    {
        this.A = Convert.ToInt32(reader.GetAttribute("A"));
        this.B = Convert.ToInt32(reader.GetAttribute("B"));
    }

    public void WriteXml(XmlWriter writer)
    {
        writer.WriteAttributeString("A", this.A.ToString());
        writer.WriteAttributeString("B", this.B.ToString());
    }
}
class Program
{
    static void Main(string[] args)
    {
        var instance = new MyClass { A = 1, B = 2 };
        Serialize(instance);
        instance = Deserialize<MyClass>();//works fine

        var list = new List<MyClass> { new MyClass { A = 10, B = 20 } };
        Serialize(list);
        list = Deserialize<List<MyClass>>();//runs forever
    }

    private static void Serialize(object o)
    {
        XmlSerializer ser = new XmlSerializer(o.GetType());
        using (TextWriter writer = new StreamWriter("xml.xml", false, Encoding.UTF8))
        {
            ser.Serialize(writer, o);
        }
    }

    private static T Deserialize<T>()
    {
        XmlSerializer ser = new XmlSerializer(typeof(T));
        using (TextReader reader = new StreamReader("xml.xml"))
        {
            return (T)ser.Deserialize(reader);
        }
    }
}

回答1:


Your problem is that, as explained in the documentation, ReadXml() must consume its wrapper element as well as its contents:

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.

MyClass.ReadXml() is not doing this, which causes an infinite loop when the MyClass object is not serialized as the root element. Instead, your MyClass must look something like this:

public class MyClass : IXmlSerializable
{
    public int A { get; set; }
    public int B { get; set; }

    public XmlSchema GetSchema()
    {
        return null;
    }

    public void ReadXml(XmlReader reader)
    {
        /*
         * https://msdn.microsoft.com/en-us/library/system.xml.serialization.ixmlserializable.readxml.aspx
         * 
         * When this method is called, the reader is positioned at the start of the element that wraps the information for your type. 
         * That is, just before 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.
         */
        var isEmptyElement = reader.IsEmptyElement;
        this.A = XmlConvert.ToInt32(reader.GetAttribute("A"));
        this.B = XmlConvert.ToInt32(reader.GetAttribute("B"));
        reader.ReadStartElement();
        if (!isEmptyElement)
        {
            reader.ReadEndElement();
        }
    }

    public void WriteXml(XmlWriter writer)
    {
        writer.WriteAttributeString("A", XmlConvert.ToString(this.A));
        writer.WriteAttributeString("B", XmlConvert.ToString(this.B));
    }
}

Now your <MyClass> element is very simple with no nested or optional elements. For more complex custom serializations there are a couple of strategies you could adopt to guarantee that your ReadXml() method reads exactly as much as it should, no more and no less.

Firstly, you could call XNode.ReadFrom() to load the current element into an XElement. This requires a bit more memory than parsing directly from an XmlReader but is much easier to work with:

public class MyClass : IXmlSerializable
{
    public int A { get; set; }
    public int B { get; set; }

    public XmlSchema GetSchema()
    {
        return null;
    }

    public void ReadXml(XmlReader reader)
    {
        var element = (XElement)XNode.ReadFrom(reader);
        this.A = (int)element.Attribute("A");
        this.B = (int)element.Attribute("B");
    }

    public void WriteXml(XmlWriter writer)
    {
        writer.WriteAttributeString("A", XmlConvert.ToString(this.A));
        writer.WriteAttributeString("B", XmlConvert.ToString(this.B));
    }
}

Secondly, you could use XmlReader.ReadSubtree() to ensure the required XML content is consumed:

public class MyClass : IXmlSerializable
{
    public int A { get; set; }
    public int B { get; set; }

    public XmlSchema GetSchema()
    {
        return null;
    }

    protected virtual void ReadXmlSubtree(XmlReader reader)
    {
        this.A = XmlConvert.ToInt32(reader.GetAttribute("A"));
        this.B = XmlConvert.ToInt32(reader.GetAttribute("B"));
    }

    public void ReadXml(XmlReader reader)
    {
        // Consume all child nodes of the current element using ReadSubtree()
        using (var subReader = reader.ReadSubtree())
        {
            subReader.MoveToContent();
            ReadXmlSubtree(subReader);
        }
        reader.Read(); // Consume the end element itself.
    }

    public void WriteXml(XmlWriter writer)
    {
        writer.WriteAttributeString("A", XmlConvert.ToString(this.A));
        writer.WriteAttributeString("B", XmlConvert.ToString(this.B));
    }
}

A few final notes:

  • Be sure to handle both <MyClass /> and <MyClass></MyClass>. These two forms are semantically identical and a sending system could chose either.

  • Prefer the methods from the XmlConvert class to convert primitives from and to XML. Doing so handles internationalization correctly.

  • Be sure to test with and without indentation. Sometimes a ReadXml() method will consume an extra XML node but the bug will be hidden when indentation is enabled -- as it is the whitespace node that gets eaten.

  • For further reading see How to Implement IXmlSerializable Correctly.



来源:https://stackoverflow.com/questions/42671036/deserializing-collection-of-types-implementing-ixmlserializable-runs-forever

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