WCF: Forward compatibility issues with DataContractSerializer

Problem statement

.Net objects are referenced in cyclic (mutual) nature.

DataContractSerializer de-serialization uses to fail in case of forward compatibility (i.e. when it tried to read from ExtensionDataObject).

Discussion

 using System.Runtime.Serialization;

Version2 Project

     [DataContract(Name = "People", Namespace = "Tests.DataContract")]

    [KnownType(typeof(Person))]

    [KnownType(typeof(AnotherPerson))]

    public class People : IExtensibleDataObject

    {

        [DataMember]

        public object Person { get; set; }

        [DataMember]

        public object AnotherPerson { get; set; }

        public ExtensionDataObject ExtensionData { get; set; }

    } 

    [DataContract(Name = "Person", Namespace = "Tests.DataContract")]

    public class Person : IExtensibleDataObject

    {

        [DataMember]

        public string Name { get; set; }

         /* This is added in this version */

        [DataMember]

        public AnotherPerson AnotherPerson { get; set; }

         public ExtensionDataObject ExtensionData { get; set; }

    }

    [DataContract(Name = "AnotherPerson", Namespace = "Tests.DataContract")]

    public class AnotherPerson : IExtensibleDataObject

    {

        [DataMember]

        public string Name { get; set; } 

        /* This is added in this version */

        [DataMember]

        public Person FriendPerson { get; set; }

        public ExtensionDataObject ExtensionData { get; set; }

    }

 ConsoleApplication

 /* ignoreExtensionDataObject is false

 * preserveObjectReferences is true

 */

            DataContractSerializer serializer = new DataContractSerializer(typeof(People), null, int.MaxValue, false, true, null, null);           

            var people = new People();

            var person = new Person() { Name = "Person" };

            var anotherPerson = new AnotherPerson() { Name = "AnotherPerson"};

            people.Person = person;

            people.AnotherPerson = anotherPerson;

            /* Cyclic references

             * person object gets a reference of anotherPerson object

             * FriendPerson property - anotherPerson object gets a reference of person object

             */

            person.AnotherPerson = anotherPerson;

            anotherPerson.FriendPerson = person;

            using (var writer = new XmlTextWriter("../../../../SavedFiles/Version2Saved.xml", null) { Formatting = Formatting.Indented })

            {

                serializer.WriteObject(writer, people);

                writer.Flush();

            }

 

Serializer.WriteObject() -> successful – means XML is generated. 

Now, we will turn back to Version1 application where it tries to de-serialize the same XML. In Version1, it will have less number of data members in comparison to the Version2 data contracts.

Version1 project

    [DataContract(Name = "People", Namespace = "Tests.DataContract")]

    [KnownType(typeof(Person))]

    [KnownType(typeof(AnotherPerson))]

    public class People : IExtensibleDataObject

    {

        [DataMember]

        public object Person { get; set; }

        [DataMember]

        public object AnotherPerson { get; set; }

        public ExtensionDataObject ExtensionData { get; set; }

    }

    [DataContract(Name = "Person", Namespace = "Tests.DataContract")]

    public class Person : IExtensibleDataObject

    {

        [DataMember]

        public string Name { get; set; } 

        public ExtensionDataObject ExtensionData { get; set; }

    } 

    [DataContract(Name = "AnotherPerson", Namespace = "Tests.DataContract")]

    public class AnotherPerson : IExtensibleDataObject

    {

        [DataMember]

        public string Name { get; set; } 

        public ExtensionDataObject ExtensionData { get; set; }

    }

ConsoleApplication

      DataContractSerializer serializer = new DataContractSerializer(typeof(People), null, int.MaxValue, false, true, null, null);

      People loadedPeople = null;

      using (var reader = new XmlTextReader("../../../../SavedFiles/Version2Saved.xml"))

                {                   

                    loadedPeople = (People)serializer.ReadObject(reader);

                }

 

Observation

1.       Serializer.ReadObject() -> fails when it tries to read data for ExtensionDataObject

2.       Serializer.ReadObject() would have been successful if we had tried with the Version2 project

3.       ReadObject API fails on the old version side. It is something to do with ExtensionDataObject.

4.       Since runtime had come across a new class structure (though same class name) to de-serialize to, it started reading data from ExtensionDataObject property.

5.       Because of cyclic reference in the XML data, dataContractSerializer was not smart enough to map to lower version of class structure.

6.       When it has to read contents from ExtensionDataObject, it looks for index number of nodes.

          In ExtensibleDataObject, GetMemberIndex() -> HandleMemberNotFound() -> HandleUnknownElement() -> ReadExtensionDataMember() -> ReadExtensionDataValue() -> ReadAndResolveUnknownXmlData() -> ReadEndElement().

          Over here - it does not find the required items from xml in sequence.

7.       This means it is a violation of forward compatibility feature.

8.       It is a design limitation in Microsoft code from the very beginning till the current version of .net (4.6.1)

9.       Internally, it is read in the following way.

 

0:001> kL

# ChildEBP RetAddr

00 002aef00 74c48fdb kernelbase!RaiseException

01 002aefa0 74c49c19 clr!RaiseTheExceptionInternalOnly+0x27f

02 002af074 7091c2e4 clr!IL_Throw+0x13e

03 002af16c 707becf1 system_runtime_serialization_ni!System.Runtime.Serialization.ObjectDataContract.ReadXmlValue(System.Runtime.Serialization.XmlReaderDelegator, System.Runtime.Serialization.XmlObjectSerializerReadContext)+0xe0

04 002af178 707beb5e system_runtime_serialization_ni!System.Runtime.Serialization.XmlObjectSerializerReadContext.ReadDataContractValue(System.Runtime.Serialization.DataContract, System.Runtime.Serialization.XmlReaderDelegator)+0x11

05 002af1b0 708f673a system_runtime_serialization_ni!System.Runtime.Serialization.XmlObjectSerializerReadContext.InternalDeserialize(System.Runtime.Serialization.XmlReaderDelegator, System.String, System.String, System.Type, System.Runtime.Serialization.DataContract ByRef)+0x7e

06 002af1dc 708eacf7 system_runtime_serialization_ni!System.Runtime.Serialization.XmlObjectSerializerReadContextComplex.InternalDeserialize(System.Runtime.Serialization.XmlReaderDelegator, System.Type, System.String, System.String)+0x5e

07 002af200 708ea162 system_runtime_serialization_ni!System.Runtime.Serialization.XmlObjectSerializerReadContext.DeserializeFromExtensionData(System.Runtime.Serialization.IDataNode, System.Type, System.String, System.String)+0xcb

08 002af220 709946b1 system_runtime_serialization_ni!System.Runtime.Serialization.XmlObjectSerializerReadContext.GetExistingObject(System.String, System.Type, System.String, System.String)+0x72

09 002af240 707beb15 system_runtime_serialization_ni!System.Runtime.Serialization.XmlObjectSerializerReadContext.TryHandleNullOrRef(System.Runtime.Serialization.XmlReaderDelegator, System.Type, System.String, System.String, System.Object ByRef)+0x1d5ac1

0a 002af284 707c8ad7 system_runtime_serialization_ni!System.Runtime.Serialization.XmlObjectSerializerReadContext.InternalDeserialize(System.Runtime.Serialization.XmlReaderDelegator, System.String, System.String, System.Type, System.Runtime.Serialization.DataContract ByRef)+0x35

0b 002af2ac 07bb0199 system_runtime_serialization_ni!System.Runtime.Serialization.XmlObjectSerializerReadContextComplex.InternalDeserialize(System.Runtime.Serialization.XmlReaderDelegator, Int32, System.RuntimeTypeHandle, System.String, System.String)+0x57

0c 002af2e8 707c238e system_runtime_serialization_ni!DynamicClass.ReadPeopleFromXml(System.Runtime.Serialization.XmlReaderDelegator, System.Runtime.Serialization.XmlObjectSerializerReadContext, System.Xml.XmlDictionaryString[], System.Xml.XmlDictionaryString[])+0x131

0d 002af300 707becf1 system_runtime_serialization_ni!System.Runtime.Serialization.ClassDataContract.ReadXmlValue(System.Runtime.Serialization.XmlReaderDelegator, System.Runtime.Serialization.XmlObjectSerializerReadContext)+0x2e

0e 002af30c 707bebd8 system_runtime_serialization_ni!System.Runtime.Serialization.XmlObjectSerializerReadContext.ReadDataContractValue(System.Runtime.Serialization.DataContract, System.Runtime.Serialization.XmlReaderDelegator)+0x11

0f 002af344 707b4526 system_runtime_serialization_ni!System.Runtime.Serialization.XmlObjectSerializerReadContext.InternalDeserialize(System.Runtime.Serialization.XmlReaderDelegator, System.String, System.String, System.Type, System.Runtime.Serialization.DataContract ByRef)+0xf8

10 002af370 707bbc6c system_runtime_serialization_ni!System.Runtime.Serialization.XmlObjectSerializerReadContextComplex.InternalDeserialize(System.Runtime.Serialization.XmlReaderDelegator, System.Type, System.Runtime.Serialization.DataContract, System.String, System.String)+0x6a

11 002af39c 707bc367 system_runtime_serialization_ni!System.Runtime.Serialization.DataContractSerializer.InternalReadObject(System.Runtime.Serialization.XmlReaderDelegator, Boolean, System.Runtime.Serialization.DataContractResolver)+0xec

12 002af3f0 708f0664 system_runtime_serialization_ni!System.Runtime.Serialization.XmlObjectSerializer.ReadObjectHandleExceptions(System.Runtime.Serialization.XmlReaderDelegator, Boolean, System.Runtime.Serialization.DataContractResolver)+0x57

13 002af40c 002e0127 system_runtime_serialization_ni!System.Runtime.Serialization.DataContractSerializer.ReadObject(System.Xml.XmlReader)+0x2c

14 002af478 74af2552 version1!Version1.Program.Main(System.String[])+0xd7

15 002af484 74aff237 clr!CallDescrWorkerInternal+0x34

16 002af4d8 74afff60 clr!CallDescrWorkerWithHandler+0x6b

17 002af550 74c1671c clr!MethodDescCallSite::CallTargetWorker+0x152

18 (Inline) -------- clr!MethodDescCallSite::Call+0xf

19 002af674 74c16840 clr!RunMain+0x1aa

1a 002af8e8 74c53dc5 clr!Assembly::ExecuteMainMethod+0x124

1b 002afde8 74c53e68 clr!SystemDomain::ExecuteMainMethod+0x63c

1c 002afe40 74c53f7a clr!ExecuteEXE+0x4c

1d 002afe80 74c56b86 clr!_CorExeMainInternal+0xdc

1e 002afebc 7519ffcc clr!_CorExeMain+0x4d

1f 002afef8 75217f16 mscoreei!_CorExeMain+0x10a

20 002aff08 75214de3 mscoree!ShellShim__CorExeMain+0x99

21 002aff10 7628336a mscoree!_CorExeMain_Exported+0x8

22 002aff1c 77a59882 kernel32!BaseThreadInitThunk+0xe

23 002aff5c 77a59855 ntdll!__RtlUserThreadStart+0x70

24 002aff74 00000000 ntdll!_RtlUserThreadStart+0x1b

 

Exception type: System.Runtime.Serialization.SerializationException

Message: Element AnotherPerson from namespace Tests.DataContract cannot have child contents to be deserialized as an object. Please use XmlNode[] to deserialize this pattern of XML.

InnerException:

Exception type: System.Xml.XmlException

Message: 'Element' is an invalid XmlNodeType.

StackTrace:

system_xml_ni!System.Xml.XmlReader.ReadEndElement()+0x55d416

system_runtime_serialization_ni!System.Runtime.Serialization.XmlReaderDelegator.ReadEndElement()+0x18

system_runtime_serialization_ni!System.Runtime.Serialization.ObjectDataContract.ReadXmlValue(System.Runtime.Serialization.XmlReaderDelegator, System.Runtime.Serialization.XmlObjectSerializerReadContext)

 

Source code

       public int GetMemberIndex(XmlReaderDelegator xmlReader, XmlDictionaryString[] memberNames, XmlDictionaryString[] memberNamespaces, int memberIndex, ExtensionDataObject extensionData)      

        {

            for (int i = memberIndex + 1; i < memberNames.Length; i++)

            {

                if (xmlReader.IsStartElement(memberNames[i], memberNamespaces[i]))

                    return i;

            }

            HandleMemberNotFound(xmlReader, extensionData, memberIndex); <--------------------------

            return memberNames.Length;

        } 

        protected void HandleMemberNotFound(XmlReaderDelegator xmlReader, ExtensionDataObject extensionData, int memberIndex)

        {

            xmlReader.MoveToContent();

            if (xmlReader.NodeType != XmlNodeType.Element)

                throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(CreateUnexpectedStateException(XmlNodeType.Element, xmlReader)); 

            if (IgnoreExtensionDataObject || extensionData == null)

                SkipUnknownElement(xmlReader);

            else

                HandleUnknownElement(xmlReader, extensionData, memberIndex); <---------------------------

        } 

        internal void HandleUnknownElement(XmlReaderDelegator xmlReader, ExtensionDataObject extensionData, int memberIndex)

        {

            if (extensionData.Members == null)

                extensionData.Members = new List<ExtensionDataMember>();

            extensionData.Members.Add(ReadExtensionDataMember(xmlReader, memberIndex)); <-------------------------

        } 

        ExtensionDataMember ReadExtensionDataMember(XmlReaderDelegator xmlReader, int memberIndex)

        {

            ExtensionDataMember member = new ExtensionDataMember();

            member.Name = xmlReader.LocalName;

            member.Namespace = xmlReader.NamespaceURI;

            member.MemberIndex = memberIndex;

            if (xmlReader.UnderlyingExtensionDataReader != null)

            {

                // no need to re-read extension data structure

                member.Value = xmlReader.UnderlyingExtensionDataReader.GetCurrentNode();

            }

            else

                member.Value = ReadExtensionDataValue(xmlReader); <----------------------------------

            return member;

        }

 

Hopefully, this design limitation of DataContract de-serialization for cyclic reference objects will be addressed in near future.

 

 

I hope this helps!