Breaking Changes are No Big Deal
So, here's the setup. Your boss comes to you and says, "We need an organized way to handle breaking changes in classes defined in a shared library when an object of that class type crosses a serialization boundary and is deserialized from one version of the class to another." What do you do?
If you are like me, at the same time as you are looking at him/her like they have just gone bonkers, you are simultaneously doing some quick math to figure out how many days you can stall your boss so that you can find a new job where they don't ask impossible things of you. Your next instinct might be to tell your boss, "Just tell them not to make breaking changes and everything will be fine."
I like a challenge, though. After some thorough research, it turns out that a combination of some of the tools in .NET 3.0's Windows Communication Foundation library will do the job and do it well.
The Problem
Here's a better explanation of the issue. Let's say that you have two websites that share session data (you probably shouldn't be doing this, but stick with me). You have defined a Person object in a shared library that both websites use. Your object looks like this:
For years, Website1 has put a Person object in Session and Website2 has deserialized the Person object, no problem. The version of the shared library they were using was identical so all problems could be avoided. The serialization story they were looking at acted a little bit like this:
This year, though, Website2 has decided to upgrade and due to some new internal regulations has removed Social Security numbers from their system. They have changed the shared Person object to look like this:
Which they didn't realize would cause a serialization and deserialization that would look and act a bit like this (read: Mushroom Cloud = Not Good):
So, what your boss is telling you is not only are we going to handle a potentially mushroom cloud causing situation, but he wants you to support it at runtime. And not just the "We're sorry, someone seems to have screwed up. Please get under your desk and kiss your ass goodbye." error message type support... we are talking full on system-just-keeps-on-trucking type handling.
In addition to the above scenario of a strictly Website1 to Website2 scenario, what if Website2 made changes to Person and reserialized it into Session? Do we want to lose the SSN property just because Person got round-tripped? No. Sound impossible to handle?
The Solution
To solve this problem, we'll use three of WCF's features. They are:
- IExtensibleDataObject Interface
- DataContracts
- DataContract Serializer
The IExtensibleDataObject interface provides a type of "state bag" for properties that are deserialized but have no home in the version of the object in the current context. So, in the above explosive scenario, but this time with the IExtensibleDataObject interface implemented, we end up with something a little more pleasant:
Any "extra properties" are maintained with the object as it is used on the site with a version of the object without matching properties and deserialized back into its original state when it is round-tripped back to its origin:
So this will take care of any issues you have with extra properties.
This is a fine solution, but the astute among you will notice that there is a scenario (well, more than "a', but that's another blog) that we haven't talked about yet. What if you serialize an object with less properties to a context that has more properties? What value does the extra property have?
Luckily, there is a way around this problem.
To initialize a newly-added property to a default value when the originating version did not contain this property, you cannot rely upon the type's constructor to set the value. You can do the next best thing, however, by creating a method that accepts a StreamingContext type and marking it with the OnDeserializing attribute, like so:
[DataContract]
public class Person
{
[DataMember]
public string FirstName;
[DataMember]
public string LastName;
[DataMember(Order=2)]
public string SSN;
[OnDeserializing]
private void SetDefaultValues(StreamingContext context)
{
SSN = "000-00-0000";
}
}
The method marked with OnDeserializing will be called before the object is deserialized, so that it can then be overwritten by a legitimate value from the serialized object.
To tie everything together, you can explicitly use the DataContractSerializer to serialize and deserialize the versions of your object. Here, we are serializing an instance of a Person into a MemoryStream:
DataContractSerializer serializer =
new DataContractSerializer(typeof(Person));
MemoryStream serializationBoundary = new MemoryStream();
serializer.WriteObject(serializationBoundary, personInstance);
And here we are deserializing it:
DataContractSerializer deserializer =
new DataContractSerializer(typeof(Person));
//here, serializationBoundary is a Stream whose
//buffer contains the serialized version of the object
object o = deserializer.ReadObject(serializationBoundary);
Person deserializedPerson = o as Person;
For more Data Contact guidelines, I found the MSDN article Best Practices: Data Contract Versioning useful.
Conclusion
So, there you have it. A combination of IExtensibleDataObject implementation, plus planning for and managing a DataContract will allow you to use the DataContractSerializer to make breaking changes no big deal.
Most of the issues in this article are addressed elsewhere only in the context of WCF services, but I have found this feature useful on its own, which is why I wanted to present it as a solution for all potential versioning issues across serialization boundaries.
I hope you found this useful. I must say that I've been extremely pleased with the power the .NET Framework 3.0 features enables. It's a good feeling to go into something thinking you have days of coding ahead of you only to realize someone has already got your back.
Trackbacks
No Trackbacks
Comments
BozoJoe
: