Welcome to Catalyst Blogs Sign in | Join | Help
DataContract Serialization and Inheritance 'gotchas'

I was giving a demonstration today of versioning DataContracts. During the demo, I made a big mistake and couldn’t figure out where I had gone wrong. I wound up promising the folks in my class that I would diagnose the issue and explain to them the error I had made. In the end, I think this is an error many of us could make without realizing it.

What I did was start out with a first generation of a typical example DataContract. The DataContract supported unknown data coming in via IExtensibleDataObject and it knew how to handle itself and any derived type from its own assembly via the special usage of the KnownTypeAttribute. Note—the KnownTypeAttribute allows one to specify a static function that can return all the types that are known to work with the class in question. The implementation is simple and, dare I say, elegant:

[DataContract(Namespace =

   "http://www.catalystss.com/Group/Product/2007/10/")]

[KnownType("Bob")]

class Person : IExtensibleDataObject

{

    string _firstName;

    string _middleInitial;

    string _lastName;

    ExtensionDataObject _extensionData;

 

    [DataMember]

    public string FirstName

    {

        get { return _firstName; }

        set { _firstName = value; }

    }

 

    [DataMember(IsRequired=true)]

    public string LastName

    {

        get { return _lastName; }

        set { _lastName = value; }

    }

 

 

    [DataMember]

    public string MiddleInitial

    {

        get { return _middleInitial; }

        set { _middleInitial = value; }

    }

 

    public ExtensionDataObject ExtensionData

    {

        get

        {

            return _extensionData;

        }

        set

        {

            _extensionData = value;

        }

    }

 

    public static Type[] Bob()

    {

        List<Type> retval = new List<Type>();

        Type[] myTypes = Assembly.GetAssembly(typeof(Person)).GetTypes();

 

        foreach (Type aType in myTypes)

        {

            if (typeof(Person).IsAssignableFrom(aType))

            {

                retval.Add(aType);

            }

        }

        return retval.ToArray();

    }

}

 

Great, right? I wrote some code to read and write this type from a file, and it all worked from the start. Feeling proud, I added an enum and then derived a type from Person that used that enum:

enum Gender

{

    Male,

    Female

}

 

[DataContract(Name="Person", Namespace =

  "http://www.catalystss.com/Group/Product/2007/11/")]

class Personv2 : Person

{

    Gender _gender;

    [DataMember(IsRequired=true)]

    public Gender Gender

    {

        get { return _gender; }

        set { _gender = value; }

    }

}

 

Nice, right? I even wrote some code to serialize this thing out. The code read as follows:

DataContractSerializer ser = new DataContractSerializer(typeof(Personv2));

FileStream fs = new FileStream("person.xml", FileMode.CreateNew);

Personv2 personv2 = new Personv2();

personv2.FirstName = "Scott";

personv2.LastName = "Seely";

personv2.MiddleInitial = "C";

personv2.Gender = Gender.Male;

ser.WriteObject(fs, personv2);

fs.Close();

The XML went to disk just fine too!

<Person xmlns="http://www.catalystss.com/Group/Product/2007/11/"

        xmlns:i="http://www.w3.org/2001/XMLSchema-instance">

    <FirstName

        xmlns=http://www.catalystss.com/Group/Product/2007/10/

        >Scott</FirstName>

    <LastName

        xmlns=http://www.catalystss.com/Group/Product/2007/10/

        >Seely</LastName>

    <MiddleInitial

        xmlns=http://www.catalystss.com/Group/Product/2007/10/

        >C</MiddleInitial>

    <Gender>Male</Gender>

</Person>

And, this is where things fell apart on me. I tried to read the object back into a Person object like so:

DataContractSerializer ser = new DataContractSerializer(typeof(Person));

FileStream fs = new FileStream("person.xml", FileMode.Open);

Person person = (Person)ser.ReadObject(fs);

fs.Close();

XmlTextWriter xtw = new XmlTextWriter(Console.Out);

ser.WriteObject(xtw, person);

 

And I get this exception:

SerializationException: Error in line 2 position 61. Expecting element 'Person' from namespace 'http://www.catalystss.com/Group/Product/2007/10/'.. Encountered 'Element'  with name 'Person', namespace 'http://www.catalystss.com/Group/Product/2007/11/'.

This code should have worked (or so I thought). Let’s look again at the XML and see what is wrong here. The XML states that the default XML namespace for the object is http://www.catalystss.com/Group/Product/2007/11/. That’s the v2 namespace! Crud… Why is this and what should it be to make things work? Well, the code wrote out the object using the v2 namespace because we told the DataContractSerializer in the first step to use Personv2 for serialization. It did, and wrote out things correctly. We then created a DataContractSerializer to read the file back in, but used Person as the base type. By switching the types, we switched the valid representation of the object. The correct way to write out the DataContract and then read in ANY type derived from person is this instead:

DataContractSerializer ser = new DataContractSerializer(typeof(Person));

FileStream fs = new FileStream("person.xml", FileMode.CreateNew);

Personv2 personv2 = new Personv2();

personv2.FirstName = "Scott";

personv2.LastName = "Seely";

personv2.MiddleInitial = "C";

personv2.Gender = Gender.Male;

ser.WriteObject(fs, personv2);

fs.Close();

In case you didn’t see it, the change is on the first line of code—I’m using typeof(Person) instead of typeof(Personv2). In this case, the XML is written as:

<Person i:type="a:Person"

        xmlns="http://www.catalystss.com/Group/Product/2007/10/"

        xmlns:i="http://www.w3.org/2001/XMLSchema-instance"

        xmlns:a="http://www.catalystss.com/Group/Product/2007/11/">

    <FirstName>Scott</FirstName>

    <LastName>Seely</LastName>

    <MiddleInitial>C</MiddleInitial>

    <a:Gender>Male</a:Gender>

</Person>

What a difference! Now, we have information about the base type. We have the default namespace as the base type, not the derived type. Finally, the enhancements in the derived type are show as belonging to a special namespace. And yes, the reader code starts to work as well. The lesson here: if you write out an object graph using a DataContractSerializer created with a derived type, you have to read it back in using DataContractSerializer at least as derived as the first one. It should be noted that the Personv2 written out with a Person-based DataContractSerializer can be read in using a Personv2-based DataContractSerializer too. That is, the following code ALSO works:

DataContractSerializer ser = new DataContractSerializer(typeof(Personv2));

FileStream fs = new FileStream("person.xml", FileMode.Open);

Person person = (Person)ser.ReadObject(fs);

fs.Close();

XmlTextWriter xtw = new XmlTextWriter(Console.Out);

ser.WriteObject(xtw, person);

xtw.Flush();

 

And people sometimes wonder why I can’t keep all these nuances in my head! The worlds of messaging and serialization are filled with these kinds of rules. The rules are always consistent, always predictable. My problem is that there are just so many rules! For those of you who spent the last three days with me on WCF, thank you for the experience!

Posted: Thursday, October 25, 2007 11:19 PM by Scott_Seely

Comments

Anonymous comments are disabled