WCF and the Cyclical Object Referencing Problem

As usual, when you need something done right, you sometimes just can not rely on MSDN or its forums, google or any form of current literature - or even a number of Microsoft employee's on my contact lists. 

Thats just the way of it sometimes.

What am I talking about? DataContractSerialization in WCF - in reference to the sometimes essential need to send cyclical object graphs across the wire.  WCF just does not support it.  At all - Something to do with an Xml Serialization standard or some such.  Which is fine in the general run of the mill situations involved with third party SOA services publically published - well... no, not in my oppinion... it's just not good enough.

But I digress.

Ok, some of you may be asking, "What do you mean by cyclical object graphs?",

Here is crude example:

[DataContract]
public class Example
{
    [DataMember]
    public Example MeMyselfAndI { get; set; }

    public Example()
    {
        this.MeMyselfAndI = this;
    }
}

Notice the (bad example) of the MeMyselfAndI property referencing itself?  When you serialize the object any serializer object will usually spit out an exception.

But wait...  Doesn't WCF use the DataContractSerializer class?  And doesn't that support cyclical object graphs?  Yes it does, which is great.  It's just that WCF doesn't generate the DataContractSerializer with the PreserveReferences feature enabled, which can be a major issue on services hosted in IIS that use the .svc files, and auto-generated client proxies and/or in situations where you have absolutely no choice but to use WCF configuration files - the M$ recommended route - and rightly so.

There are plenty of examples on the web demonstrating how to use the DataContractSerializer/PreserveReferences purely on the client (and only the client so far as I can find) when building your WCF client manually sans config.  This however only solves half of the problem because you just can not get the server to play the same way with these examples if you need cyclical object graphs returned, and the lack of a configuration support flies in the face of all reason.

MSDN is simply no help, neither are the forums, neither are a couple of highly skilled Microsoft employed architects I know.  *grin* they will choke when I send them the link to this.  I tend to enjoy these moments...

So, here is the solution... here is how to get WCF to support PreserveReferences... and still maintain the configurational aspect of WCF.

First, the server

The mechanics of WCF hosted in IIS involves the use of the well known '.svc' file.  Behind the scenes within WCF each page is generated within a factory that generates a ServiceHost instance.  The service host handles the requested calls to your service as well as all the appropriate discriptive information required during the lifetime of the service request. 

Usually, the ServiceHost is completely handled by the activation classes within the the ASP.NET model (Microsoft say it's handled by IIS... not true... but makes things easy to explain I suppose).  The ServiceHost contains all the information about the service contracts etc.  However, the ServiceHost isn't generally available under normal circumstances - and we need the ServiceHost.

So, to get access to the ServiceHost, we need to create our own ServiceHost factory and tell the '.svc' to it instead of the default factory supplied by WCF (code follows shortly).  To tell the '.svc' file to use the custom factory we do the following:

Remove the codebehind attribute and replace it with the Factory attribute thus...

<%@ ServiceHost Language="C#" Debug="true" Service="TestWCF.ManagerService" Factory="TestWCF.ManagerServiceHostFactory"  %>

Don't worry about what visual studio says about the Factory attribute not being valid, it is supported by WCF in the internal System.ServiceModel.Activation.ServiceParser class, which is more important.

The custom ServiceHostFactory, ServiceHost and a minor DataContactSerializer replacement are as follows:

using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Description;
using System.Xml;

public class ManagerServiceHostFactory : ServiceHostFactory
{
    protected override ServiceHost CreateServiceHost(Type serviceType, 
Uri[] baseAddresses) { ManagerServiceCustomHost customServiceHost
= new ManagerServiceCustomHost(serviceType, baseAddresses);


return customServiceHost; } } public class ManagerServiceCustomHost : ServiceHost { public ManagerServiceCustomHost(Type serviceType, params Uri[] baseAddresses) : base(serviceType, baseAddresses) { } protected override void ApplyConfiguration() { base.ApplyConfiguration(); RefPreserveDataContractSerializerOperationBehavior.Reconfigure
(base.ImplementedContracts["TestWCF.IManagerService"].Operations); } } public class RefPreserveDataContractSerializerOperationBehavior
: DataContractSerializerOperationBehavior { public static void Reconfigure(OperationDescriptionCollection odc) { foreach (OperationDescription oper in odc) { for (int i = 0; i < oper.Behaviors.Count; i++) { if (oper.Behaviors[i] is DataContractSerializerOperationBehavior) { DataContractSerializerOperationBehavior tmp
= oper.Behaviors[i]
as DataContractSerializerOperationBehavior; // replace out the DataContractSerializerOperationBehavior
// with our own...
RefPreserveDataContractSerializerOperationBehavior tmp2
= new RefPreserveDataContractSerializerOperationBehavior
(oper, tmp.DataContractFormatAttribute); tmp2.DataContractSurrogate = tmp.DataContractSurrogate; tmp2.IgnoreExtensionDataObject = tmp.IgnoreExtensionDataObject; tmp2.MaxItemsInObjectGraph = tmp.MaxItemsInObjectGraph; oper.Behaviors[i] = tmp2; break; } } } } public RefPreserveDataContractSerializerOperationBehavior
(OperationDescription operationDescription) : this(operationDescription, null) { } public RefPreserveDataContractSerializerOperationBehavior
(OperationDescription operation,
DataContractFormatAttribute
dataContractFormatAttribute
) : base(operation, dataContractFormatAttribute) { } public override XmlObjectSerializer CreateSerializer
(Type type, string name, string ns, IList<Type> knownTypes) { return CreateDataContractSerializer(type, name, ns, knownTypes); } private static XmlObjectSerializer CreateDataContractSerializer
(Type type, string name, string ns, IList<Type> knownTypes) { return CreateDataContractSerializer(type, name, ns, knownTypes); } public override XmlObjectSerializer CreateSerializer
(Type type, XmlDictionaryString name, XmlDictionaryString ns,
IList<Type> knownTypes) { return new DataContractSerializer
(type, name, ns, knownTypes, int.MaxValue,
false,
true /* PreserveReferences*/,
null); } }

You should notice in the DataContractSerializer the 'static void Reconfigure(OperationDescriptionCollection odc)' method.  This is what does the work to ensure that WCF gives us that nice cyclical reference feature we are after.  Also please notice the contructor parameters used in the CreateSerializer method (please see MSDN for more info).

[client work pending... need to eat... ;)]