Serializing Cyclic Graphs of objects with WCF

*** This is a repost of a previous post because of moving to a new blog engine in which some formatting was lost ***

Abstract

In this blog post I will be discussing how the serialize cyclic graphs of objects using WCF, including data contracts and POCO’s ( Plain Old CLR Objects).

Introduction

WCF allows you to serialize any number of objects over the network or to disk using the DataContractSerializer class. However, some issues can arise when you have cycles in your object graph, and especially when you want some shared objects to stay shared!

Serializing cyclic objects

Let’s look at this with a couple of examples. First I want some datacontracts with possible cycles:

  [DataContract(Namespace=Namespaces.U2U, Name="address", IsReference=false)]
  public class Address
  {
    [DataMember(Name="street", Order=1)]
    public string Street { get; set; }
    [DataMember(Name="city", Order=2)]
    public string City { get; set; }
  }

  [DataContract(Namespace = Namespaces.U2U, Name = "person", IsReference = false)]
  public class Person
  {
    [DataMember(Name="name", Order=1)]
    public string Name { get; set; }
    [DataMember(Name="age", Order=2)]
    public int Age { get; set; }
    [DataMember(Name="partner",Order=3)]
    public Person Partner { get; set; }
    [DataMember(Name = "address", Order = 4)]
    public Address Address { get; set; }
  }

 

I’ll create a person object with a reference to another person. Since they are partners, they will point to each other.

image

DataContractSerializer ser = new DataContractSerializer(typeof(Person));
string path = Path.Combine( Environment.CurrentDirectory, "cartoon.xml" );
using (FileStream ms = File.Open(path, FileMode.Create, FileAccess.Write))
{
  Address home = new Address { Street = "ToonLane", City = "ToonTown" };
  Person tom = new Person { Name = "Tom", Age = 4, Address = home };
  Person jerry = new Person { Name = "Jerry", Age = 2, Address = home };
  tom.Partner = jerry;
  jerry.Partner = tom;
  ser.WriteObject(ms, tom);
}

So what happens when you try this? You’ll get an exception:

image

[NOTE: By the way, this is much better then in .NET 1.0 with the XmlSerializer, it would serialize the first object, then follow the link to the second, then back the to first, then back to the second, until your disk ran out of space :) Now XmlSerializer will also throw an exception if you try to serialize cycles.]

Using IsReference

To make the exception go away, you add IsReference=true to the Person’s DataContract:

The result now looks like this:

<person z:Id="i1" 
xmlns="urn://www.u2u.be/samples/wcf/2009"
xmlns:i=http://www.w3.org/2001/XMLSchema-instance
xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/"> <name>Tom</name> <age>4</age> <partner z:Id="i2"> <name>Jerry</name> <age>2</age> <partner z:Ref="i1"/> <address> <street>ToonLane</street> <city>ToonTown</city> </address> </partner> <address> <street>ToonLane</street> <city>ToonTown</city> </address> </person>

Look how Jerry refers back to Tom using the z:Ref! Now you can easily send objects with cycles. But wait! Both point to the same address, but now Tom and Jerry both have an address, although identical.

image

Again we can change this by changing the IsReference to true in the Address DataContract. Now the result looks like this:

<person z:Id="i1" xmlns="urn://www.u2u.be/samples/wcf/2009" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/">
  <name>Tom</name>
  <age>4</age>
  <partner z:Id="i2">
    <name>Jerry</name>
    <age>2</age>
    <partner z:Ref="i1"/>
    <address z:Id="i3">
      <street>ToonLane</street>
      <city>ToonTown</city>
    </address>
  </partner>
  <address z:Ref="i3"/>
</person>

Please note that now both Tom and Jerry use the same address instance, not clones.

Serializing cyclic POCO’s with WCF

“And what about POCO’s? (Plain Old Clr Objects) I hear you say. Well in that case you cannot attach the DataContract attribute of course, so you‘ll need to use another variant of the DataContractSerializer constructor:

new DataContractSerializer(typeof(PocoPerson), null, int.MaxValue, false
                          , /* preserveObjectReferences */ true, null, null);

So, using this constructor the previous example looks like this:

  public class PocoPerson
  {
    public string Name { get; set; }
    public int Age { get; set; }
    public PocoPerson Partner { get; set; }
    public PocoAddress Address { get; set; }
  }

  public class PocoAddress
  {
    public string Street { get; set; }
    public string City { get; set; }
  }
  DataContractSerializer ser = 
    new DataContractSerializer(typeof(PocoPerson), null, int.MaxValue, false
                              , /* preserveObjectReferences */ true, null, null);
  string path = Path.Combine(Environment.CurrentDirectory, "cartoon2.xml");
  using (FileStream ms = File.Open(path, FileMode.Create, FileAccess.Write))
  {
    PocoAddress home = new PocoAddress { Street = "ToonLane", City = "ToonTown" };
    PocoPerson tom = new PocoPerson { Name = "Tom", Age = 4, Address = home };
    PocoPerson jerry = new PocoPerson { Name = "Jerry", Age = 2, Address = home };
    tom.Partner = jerry;
    jerry.Partner = tom;
    ser.WriteObject(ms, tom);
}

Running this will result in following Xml:

<PocoPerson z:Id="1" 
            xmlns="http://schemas.datacontract.org/2004/07/ReferenceSerialization" 
            xmlns:i="http://www.w3.org/2001/XMLSchema-instance" 
            xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/">
  <Address z:Id="2">
    <City z:Id="3">ToonTown</City>
    <Street z:Id="4">ToonLane</Street>
  </Address>
  <Age>4</Age>
  <Name z:Id="5">Tom</Name>
  <Partner z:Id="6">
    <Address z:Ref="2" i:nil="true"/>
    <Age>2</Age>
    <Name z:Id="7">Jerry</Name>
    <Partner z:Ref="1" i:nil="true"/>
  </Partner>
</PocoPerson>

Using a custom behavior

Of course it is not that easy to use this contructor inside a real service, because you’re not in charge of creating the serializer. But hey, WCF is very extensible right? So to plug the hole, you need to build your own Behavior, specifically a DataContractSerializerOperationBehavior.

public class CyclicSerializationBehavior : DataContractSerializerOperationBehavior
{
  public CyclicSerializationBehavior(OperationDescription od)
    : base(od)
  { }

  private const bool preserveObjectReferences = true;

  public override XmlObjectSerializer CreateSerializer(
Type type, XmlDictionaryString name, XmlDictionaryString ns, IList<Type> knownTypes) { return new DataContractSerializer(type, name, ns, knownTypes,
this.MaxItemsInObjectGraph, this.IgnoreExtensionDataObject,
preserveObjectReferences, this.DataContractSurrogate); } public override XmlObjectSerializer CreateSerializer(
Type type, string name, string ns, IList<Type> knownTypes) { return new DataContractSerializer(type, name, ns, knownTypes,
this.MaxItemsInObjectGraph, this.IgnoreExtensionDataObject,
preserveObjectReferences, this.DataContractSurrogate); } }

To get this behavior installed you need to write some code to add it the the operation’s behavior, or even better is to use an IContractBehavior:

  [AttributeUsage(AttributeTargets.All)]
  public class CyclicSerializationAttribute : Attribute, IContractBehavior
  {
    public void AddBindingParameters(ContractDescription contractDescription, 
ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { } public void ApplyClientBehavior(ContractDescription contractDescription,
ServiceEndpoint endpoint, ClientRuntime clientRuntime) { ReplaceDataContractSerializerOperationBehavior(contractDescription); } public void ApplyDispatchBehavior(ContractDescription contractDescription,
ServiceEndpoint endpoint, DispatchRuntime dispatchRuntime) { ReplaceDataContractSerializerOperationBehavior(contractDescription); } private void ReplaceDataContractSerializerOperationBehavior(ContractDescription contractDescription) { foreach (OperationDescription operation in contractDescription.Operations) { DataContractSerializerOperationBehavior beh =
operation.Behaviors.Find<DataContractSerializerOperationBehavior>(); if (beh != null) { operation.Behaviors.Remove(beh); operation.Behaviors.Add(new CyclicSerializationBehavior(operation)); } } } public void Validate(ContractDescription contractDescription, ServiceEndpoint endpoint) { } }

I made this class into an attribute so you can apply it to your service contract or service class, and when WCF loads it it replaces the default DataContractSerialerOperationBehavior. At first I simply added it to the collection of behaviors, but then I got all kinds of strange things :)

So, to use this now you apply the attribute to the service contract:

  [ServiceContract]
  [CyclicSerializationAttribute]
  public interface ICyclicSerializationService
  {
    [OperationContract]
    PocoPerson GetPocoPerson();
  }

Don’t forget, you should add this to both the server en client side.

Comments are closed