共用方式為


WCF Extensibility – Other Serialization Extensions

This post is part of a series about WCF extensibility points. For a list of all previous posts and planned future ones, go to the index page.

Wrapping up the “chapter” on serialization, this post will talk about other extensions for the serialization features in WCF which are too small to deserve their own post.

IExtensibleDataObject

IExtensibleDataObject is all about versioning. Its main (sole?) purpose is round-tripping different versions of a data contract without losing information. Take a Person type, for instance, with the definition as shown below.

  1. [DataContract(Name = "Person")]
  2. public class Person
  3. {
  4.     [DataMember(Order = 0)]
  5.     public string Name { get; set; }
  6.     [DataMember(Order = 1)]
  7.     public int Age { get; set; }
  8. }

This type is used in a service which has many methods to store and retrieve this information used by clients. One client application, for instance, is responsible for birthdays, adding 1 to the person’s age and returning it back to the server. Everything works out fine.

Now there is a change in the business logic of the server (or the database schema), where the person object will also hold a reference to the address, as follows.

  1. [DataContract(Name = "Person")]
  2. public class Person_V2
  3. {
  4.     [DataMember(Order = 0)]
  5.     public string Name { get; set; }
  6.     [DataMember(Order = 1)]
  7.     public int Age { get; set; }
  8.     [DataMember(Order = 2)]
  9.     public string Address { get; set; }
  10. }

Old clients (with the original contract) still work out fine – the contract name and namespace match, the fields are in the expected order (remember, the DataContractSerializer enforces the order of the fields), and the old clients will simply ignore the extra element for the address which comes from the updated server. And new client applications can take advantage of this new field, and we can have a new app which can be used to update the client address. But the fact that the old clients ignore the address, while not a problem for themselves, can pose a problem for the system as a whole. Coming back to the client which knows about birthdays, if the client retrieves an object from the server with an Address value, it will discard that value. After it updates the person’s age and sends the Person instance back to the server, it will not have an Address field, and so it will be received as null (Nothing) in the server side.

That’s where the IExtensibleDataObject interface (IEDO) comes into play. By declaring a type as extensible (implementing the IEDO interface), the type provides to WCF a place to store any members which it doesn’t know about. When the object is reserialized, those “unknown” members are serialized back (in the same order they were received). By default, all data contract types generated using svcutil.exe or Add Service Reference implement that interface, so those clients are versioning-safe (at least with respect to additions in the data contract).

Implementing IEDO is trivial: just one property of type ExtensionDataObject – all the type needs to do is to store and return this object. ExtensionDataObject is an opaque type: it has no public properties (so you can’t set or retrieve the additional data), it’s sealed (you can’t extend it) and it doesn’t have any public constructors (you can’t create an instance of it) – it can only be manipulated by the WCF serializer internally. Here’s the same Person class, this time versioning-aware. The automatic properties in C# makes its implementation quite simple – it’s literally a one-line change addition plus the declaration that the class implements IEDO.

  1. [DataContract(Name = "Person")]
  2. public class Person : IExtensibleDataObject
  3. {
  4.     public ExtensionDataObject ExtensionData { get; set; }
  5.     [DataMember(Order = 0)]
  6.     public string Name { get; set; }
  7.     [DataMember(Order = 1)]
  8.     public int Age { get; set; }
  9. }

And that’s likely all you’ve ever need to know about IExtensibleDataObject.

IDeserializationCallback

The serialization callback attributes were introduced in .NET Framework 2.0, but since the first version there have been serializers (such as the BinaryFormatter and the SoapFormatter). In that version, there was one option for running code after the deserialization of the object graph was completed, and that was with the IDeserializationCallback interface. On .NET Framework 2.0, I imagine the designers thought that adding three more interfaces would make the class declaration overly complex, and decided to go with the attribute approach instead (this is just my theory).

IDeserializationCallback (IDC) is also a simple interface, with a single method, OnDeserialization, which is run when the entire object graph has been deserialized. It’s almost equivalent to the OnDeserializedAttribute, in a way that the callback method declared with that attribute will be invoked for an object after that object has finished deserializing its members. Take the example below, in which a Person class has two members of type Address. When we deserialize an instance using the BinaryFormatter, first the Person.OnDeserializing is called, indicating that that type is about to be deserialized. Then, the first Address.OnDeserializing is invoked, that object is deserialized (read from the stream), and Address.OnDeserialized is called for the first address. The same happens for the second address in the person: Address.OnDeserialzing, the second object is read from the stream, then Address.OnDeserialized is called. Finally, since all the members of Person have been read from the stream, Person.OnDeserialized is called. And now that the entire deserialization episode has finished, the IDeserializationCallback.OnDeserialization method is invoked on the Person object and in both Address instances.

  1. [Serializable]
  2. [DataContract]
  3. public class Person : IDeserializationCallback
  4. {
  5.     [DataMember]
  6.     public string Name;
  7.     [DataMember]
  8.     public Address Address;
  9.     [DataMember]
  10.     public Address Address2;
  11.  
  12.     [OnDeserializing]
  13.     public void OnDeserializing(StreamingContext context)
  14.     {
  15.         Console.WriteLine("In {0}.{1}", this.GetType().Name, MethodBase.GetCurrentMethod().Name);
  16.     }
  17.     [OnDeserialized]
  18.     public void OnDeserialized(StreamingContext context)
  19.     {
  20.         Console.WriteLine("In {0}.{1}", this.GetType().Name, MethodBase.GetCurrentMethod().Name);
  21.     }
  22.     public void OnDeserialization(object sender)
  23.     {
  24.         Console.WriteLine("In {0}.{1}, sender = {2}", this.GetType().Name, MethodBase.GetCurrentMethod().Name, sender);
  25.     }
  26. }
  27.  
  28. [DataContract]
  29. [Serializable]
  30. public class Address : IDeserializationCallback
  31. {
  32.     [DataMember]
  33.     public string Street;
  34.     [DataMember]
  35.     public string City;
  36.  
  37.     [OnDeserializing]
  38.     public void OnDeserializing(StreamingContext context)
  39.     {
  40.         Console.WriteLine("In {0}.{1}", this.GetType().Name, MethodBase.GetCurrentMethod().Name);
  41.     }
  42.     [OnDeserialized]
  43.     public void OnDeserialized(StreamingContext context)
  44.     {
  45.         Console.WriteLine("In {0}.{1}", this.GetType().Name, MethodBase.GetCurrentMethod().Name);
  46.     }
  47.     public void OnDeserialization(object sender)
  48.     {
  49.         Console.WriteLine("In {0}.{1}, sender = {2}", this.GetType().Name, MethodBase.GetCurrentMethod().Name, sender);
  50.     }
  51. }

Now, this is the behavior on the Binary (and Soap) Formatter classes. The IDeserializationCallback interface is also supported on the WCF serializers (DataContractSerializer, NetDataContractSerializer and DataContractJsonSerializer), but its behavior is different than on the original formatters (I have no idea why). On the WCF serializers, IDeserializationCallback.OnDeserialization is essentially equivalent to OnDeserializedAttribute, in which it’s invoked when the object is deserialized. Possibly because of this, I’ve never seen IDeserializationCallback being used in WCF, with the serialization callback attributes being used instead. And the rhetorical question: if both are used, then IDC is invoked first, then OnDeserialized (see in the code in this post for examples) on the WCF serializers, but really, just don’t use both to avoid unnecessary complications :-).

IObjectReference

Another little-used serialization extensibility, IObjectReference (IOR) is an interface which indicates that the class which implements it can return a reference to a different object after it has been deserialized. The main scenario for this interface is for when we want to have a singleton class; since the serializer bypasses the constructor, it can bypass the singleton protection of the class. But by marking the type as IOR, we can, after the serializer created the new object, tell it to use the object which we want. But its usage isn’t limited to singletons – any time we need to restrict the actual instances of a certain type, we can use this interface, as in the example below.

  1. [DataContract]
  2. [Serializable]
  3. public class Dwarf : IObjectReference
  4. {
  5.     [DataMember]
  6.     private string name;
  7.  
  8.     public static readonly Dwarf Doc = new Dwarf("Doc");
  9.     public static readonly Dwarf Grumpy = new Dwarf("Grumpy");
  10.     public static readonly Dwarf Happy = new Dwarf("Happy");
  11.     public static readonly Dwarf Sleepy = new Dwarf("Sleepy");
  12.     public static readonly Dwarf Bashful = new Dwarf("Bashful");
  13.     public static readonly Dwarf Sneezy = new Dwarf("Sneezy");
  14.     public static readonly Dwarf Dopey = new Dwarf("Dopey");
  15.  
  16.     private Dwarf() { }
  17.  
  18.     private Dwarf(string name)
  19.     {
  20.         this.name = name;
  21.     }
  22.  
  23.     public override string ToString()
  24.     {
  25.         return string.Format("Dwarf[Name={0}]", this.name);
  26.     }
  27.  
  28.     public object GetRealObject(StreamingContext context)
  29.     {
  30.         switch (this.name)
  31.         {
  32.             case "Doc":
  33.                 return Doc;
  34.             case "Grumpy":
  35.                 return Grumpy;
  36.             case "Happy":
  37.                 return Happy;
  38.             case "Sleepy":
  39.                 return Sleepy;
  40.             case "Bashful":
  41.                 return Bashful;
  42.             case "Sneezy":
  43.                 return Sneezy;
  44.             case "Dopey":
  45.                 return Dopey;
  46.             default:
  47.                 throw new InvalidOperationException("Invalid Dwarf: " + this.name);
  48.         }
  49.     }
  50. }

When the object deserialization is done, the serializer will call IObjectReference.GetRealObject, and the class can replace itself in the deserialization graph with another object. In the example above, we’ve “protected” the class by only having private constructors, but since serialization bypasses them, new objects would be created. By implementing IObjectReference, we can maintain the instancing control over the instances of the type.

Final thoughts about serialization extensibility

The serializers in WCF (and the “legacy” ones) have quite a few extensibility points, but in most cases you won’t need to use them. But if the needs is there, I hope those posts will help you to understand your options for solving those issues.

[Code in this post]

[Back to the index]