1

I have a class which can only be serialized with custom XML serialization which is to be used as a property of certain classes within a much larger system that uses basic serialization. I can’t convert the whole system to custom serialization because it’s huge and also may in the future contain third-party modules that use basic serialization.

My first question is whether this is allowed at all. I can’t see anything on MSDN that says you are not allowed to reference a custom-serialized object from a basic-serialised one, and it would seem like a serious restriction to the portability of code if you couldn’t. However an earlier answer on this site seems to suggest that you might not be able to (the poster says “You can't really mix and match serialization unfortunately; once you implement IXmlSerializable, you own everything”) although I think he is referring to mixing and matching within a class, which you clearly can't do. (See: Mixing custom and basic serialization?)

So assuming this is in fact allowed, my problem is that it fails whenever a class within the main system implements a List which contains two or more objects of the class that has custom serialization.

Interestingly, the failure only occurs on deserialization, and it only occurs when there is more than one reference to such an object. Even more intriguing, it fails in the same way even if the list appears further up the chain of dependencies (e.g. a list contains normal objects, which contain normal objects, which may contain objects that use custom serialization).

I have written a little test program that demonstrates the simplest case, as below.

So my questions are:

  1. Is it in fact allowed to reference custom-serialized objects from basic-serialized ones?
  2. If it is, am I doing something stupid?
  3. If not, is this a known bug?

NOTES ON THE TEST PROGRAM: The data class structure is very simple and consists of class BasicXml (which uses basic serialization) and class CustomXml (which uses custom serialization). BasicXml contains a list of CustomXml. The other class contains the test, which is self-standing. Just instantiate and run RunTests().

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Schema;
using System.Xml.Serialization;

namespace AdHocTests
{
    [Serializable]
    public class BasicXml
    {
        public List<CustomXml> TestList { get; set; }
    }
    [Serializable]
    public class CustomXml : IXmlSerializable
    {
        public CustomXml() { }
        public CustomXml(string name)
        {
            Name = name;
        }
        public string Name { get; set; }
        public void ReadXml(XmlReader reader)
        {
            Name = reader.ReadString();
        }
        public void WriteXml(XmlWriter writer)
        {
            writer.WriteString(Name);
        }
        public XmlSchema GetSchema()
        {
            return null; // I have removed this code for clarity
        }
    }
    public class MixedSerializationTest
    {
        public MixedSerializationTest()
        {
            _serializer.UnknownElement += UnknownElementHandler;
        }
        public void RunTests()
        {
            RunOneTest(makeItFail: false);
            RunOneTest(makeItFail: true);
        }
        private void RunOneTest(bool makeItFail)
        {
            Debug.Write("\n\nRUNNING TEST THAT WILL " + (makeItFail ? "FAIL" : "PASS") + ":\n\n");
            CustomXml c1 = new CustomXml("Hello");
            CustomXml c2 = new CustomXml("World");
            BasicXml b1 = new BasicXml
            {
                TestList = makeItFail ? new List<CustomXml> { c1, c2 } : new List<CustomXml> { c1 }
            };
            XElement xml1 = GetXmlFromObject(b1);
            Debug.Write("Serialized XML:\n" + xml1.ToString() + "\n=====\n");
            BasicXml b2 = GetObjectFromXml(xml1);
            if (_cancelDeserialization) return;
            XElement xml2 = GetXmlFromObject(b2);
            Debug.Write("Reserialized XML:\n" + xml2.ToString() + "\n=====\n");
        }
        private XElement GetXmlFromObject(BasicXml obj)
        {
            using (StringWriter sw = new StringWriter())
            {
                using (XmlWriter xw = XmlWriter.Create(sw))
                {
                    _serializer.Serialize(xw, obj);
                    return XElement.Parse(sw.ToString());
                }
            }
        }
        private BasicXml GetObjectFromXml(XElement xml)
        {
            using (StringReader sr = new StringReader(xml.ToString()))
            {
                XmlWriterSettings settings = new XmlWriterSettings();
                using (XmlReader xr = XmlReader.Create(sr))
                {
                    return (BasicXml)_serializer.Deserialize(xr);
                }
            }
        }
        private void UnknownElementHandler(object sender, XmlElementEventArgs e)
        {
            Debug.Write("\n*** Serializer threw an UnknownElement exception ***\n\n");
            _cancelDeserialization = true;
        }
        private XmlSerializer _serializer = new XmlSerializer(typeof(BasicXml));
        private bool _cancelDeserialization = false;
    }
}
Community
  • 1
  • 1
Geoffff
  • 145
  • 1
  • 7
  • Note that if CustomXml does *not* implement IXmlSerializable, it works, which suggests you're missing some things. – RamblinRose Dec 09 '16 at 00:08
  • 1
    Nice [mcve]! I was able to reproduce the problem right away. – dbc Dec 09 '16 at 00:58
  • Hi RamblinRose, well removing the IXmlSerializable interface is the switch that turns it back to basic serialization rather than custom, so yes it works then because it's been turned back to the normal basic + basic situation. And when the interface is in place (indicating a basic + custom situation) it works nearly all the time, and always serialises perfectly, which suggests that the code is probably right. It's just that it does this weird thing under the very special circumstances I've laid out. – Geoffff Dec 09 '16 at 01:01
  • Thanks for the feedback, dbc – Geoffff Dec 09 '16 at 01:03

1 Answers1

1

You certainly can include instances of IXmlSerializable types contained in some larger object graph passed to XmlSerializer.

Your problem is as follows. In your ReadXml() method, you need to call XmlReader.ReadElementContentAsString() instead of XmlReader.ReadString():

public void ReadXml(XmlReader reader)
{
    Name = reader.ReadElementContentAsString();
}

As explained in this answer:

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 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.

Thus you need to be sure to consume the </CustomXml> end element tag. And, according to the docs for ReadElementContentAsString(), it does just what you need:

This method reads the start tag, the contents of the element, and moves the reader past the end element tag.

On the other hand the docs for ReadString() state:

We recommend that you use the ReadElementContentAsString method to read the contents of an element or text node as a string.

So stay away from that one.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • That's awesome, thanks dbc, that one small change fixes it completely, and it also explains why what I was seeing was "unexpected runtime exceptions or corrupt data" - always difficult to pin down! And a very clear explanation too, thanks. – Geoffff Dec 09 '16 at 01:20