I learned something new today: Starting with .NET 3.5, the XmlSerializer no longer serializes properties that are marked with the Obsolete attribute. I can’t say that I really agree with this. Marking something Obsolete is supposed to be something for a developer to deal with in source code. Once an object is serialized to XML, it becomes data. I think using the Obsolete attribute as both a compiler flag as well as controlling XML serialization is a bad idea. In this post, I’ll show you how I ran into this and how I got around it. The Setup Let’s start with some make-believe code to demonstrate the issue. We have a simple data class for storing some information. We use XML serialization to read and write the data: public class MyData
{
public int Age { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public List<String> Hobbies { get; set; }
public MyData()
{
this.Hobbies = new List<string>();
}
}
Now a few simple lines of code to serialize it to XML:
static void Main(string[] args)
{
var data = new MyData
{
FirstName = "Zachary",
LastName = "Smith",
Age = 50,
Hobbies = {"Mischief", "Sabotage"},
};
var serializer = new XmlSerializer(typeof (MyData));
serializer.Serialize(Console.Out, data);
Console.ReadKey();
}
And this is what we see on the console:
<?xml version="1.0" encoding="IBM437"?>
<MyData xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<Age>50</Age>
<FirstName>Zachary</FirstName>
<LastName>Smith</LastName>
<Hobbies>
<string>Mischief</string>
<string>Sabotage</string>
</Hobbies>
</MyData>
The Change
So we decided to track the hobbies as a list of strings. As always, things change and we have more information we need to store per-hobby. We create a custom “Hobby” object, add a List<Hobby> to our MyData class and we obsolete the old “Hobbies” list to let developers know they shouldn’t use it going forward:
public class Hobby
{
public string Name { get; set; }
public int Frequency { get; set; }
public int TimesCaught { get; set; }
public override string ToString()
{
return this.Name;
}
}
public class MyData
{
public int Age { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
[Obsolete("Use HobbyData collection instead.")]
public List<String> Hobbies { get; set; }
public List<Hobby> HobbyData { get; set; }
public MyData()
{
this.Hobbies = new List<string>();
this.HobbyData = new List<Hobby>();
}
}
Here’s the kicker: This serialization is done in another application. The consumers of the XML will be older clients (clients that expect only a “Hobbies” collection) as well as newer clients (that support the new “HobbyData” collection). This really shouldn’t be a problem – the obsolete attribute is metadata for .NET compilers. Unfortunately, the XmlSerializer also looks at the compiler attribute to determine what items to serialize/deserialize. Here’s an example of our problem:
static void Main(string[] args)
{
var xml = @"<?xml version=""1.0"" encoding=""IBM437""?>
<MyData xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema"">
<Age>50</Age>
<FirstName>Zachary</FirstName>
<LastName>Smith</LastName>
<Hobbies>
<string>Mischief</string>
<string>Sabotage</string>
</Hobbies>
</MyData>";
var serializer = new XmlSerializer(typeof(MyData));
var stream = new StringReader(xml);
var data = (MyData) serializer.Deserialize(stream);
if( data.Hobbies.Count != 2)
{
throw new ApplicationException("Hobbies did not deserialize properly");
}
}
If you run the code above, you’ll hit the exception. Even though the XML contains a “<Hobbies>” node, the obsolete attribute prevents the node from being processed. This will break old clients that use the new library, but don’t yet access the HobbyData collection.
The Fix
This fix (in this case), isn’t too painful. The XmlSerializer exposes events for times when it runs into items (Elements, Attributes, Nodes, etc…) it doesn’t know what to do with. We can hook in to those events and check and see if we’re getting something that we want to support (like our “Hobbies” node).
Here’s a way to read in the old XML data with full support of the new data structure (and keeping the Hobbies collection marked as obsolete):
static void Main(string[] args)
{
var xml = @"<?xml version=""1.0"" encoding=""IBM437""?>
<MyData xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema"">
<Age>50</Age>
<FirstName>Zachary</FirstName>
<LastName>Smith</LastName>
<Hobbies>
<string>Mischief</string>
<string>Sabotage</string>
</Hobbies>
</MyData>";
var serializer = new XmlSerializer(typeof(MyData));
serializer.UnknownElement += serializer_UnknownElement;
var stream = new StringReader(xml);
var data = (MyData)serializer.Deserialize(stream);
if (data.Hobbies.Count != 2)
{
throw new ApplicationException("Hobbies did not deserialize properly");
}
}
static void serializer_UnknownElement(object sender, XmlElementEventArgs e)
{
if( e.Element.Name != "Hobbies")
{
return;
}
var target = (MyData) e.ObjectBeingDeserialized;
foreach(XmlElement hobby in e.Element.ChildNodes)
{
target.Hobbies.Add(hobby.InnerText);
target.HobbyData.Add(new Hobby{Name = hobby.InnerText});
}
}
As you can see, we hook in to the “UnknownElement” event. Once we determine it’s our “Hobbies” node, we deserialize it ourselves – as well as populating the new HobbyData collection. In this case, we have a fairly simple solution to a small change in XML layout. If you make more extensive changes, it would probably be easier to do some custom serialization to support older data.
A sample project with all of this code is available from my repository on bitbucket.
Technorati Tags: XmlSerializer,Obsolete,.NET