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!
Comments
- Anonymous
January 09, 2016
This behavior is by design. If you need full fidelity for self-referencing object graphs then you can use NetDataContractSerializer, for which I (and others) lobbied long and hard to keep in WCF back when it was being created.