Sunday 23 August 2009

Changing the type of a returned object in WCF

[Download Full Solution]

Every once and a while the returned object type from a service is too simple/generic, and a more rich, or specialized object would be a better fit. This is typically easy to achieve by instructing a new type that's comparable with the same service contract to be the resulting type. What about if one XML representation could best be described as multiple classes though? What this article tries to demonstrate is how to closely manage the DataContractSerializer in such a way that the returned type through the WCF endpoint is the desired type, while remapping it at the level of the serializer if appropriate.

What we have below is how we are going to update the DataContractSerializer to do our bidding. We are going to create a new implementation of IEndpointBehavior which will update the DataContractSerializer on the operational contracts of the assigned endpoint.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <extensions>
      <behaviorExtensions>
        <add name="specializedType" type="CA.NGommans.Serialization.SpecializedTypeBehaviorExtensionElement, SerializationDemoClient, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
      </behaviorExtensions>
    </extensions>
...
    <behaviors>
      <endpointBehaviors>
        <behavior name="specializedTypes">
          <specializedType>
            <knownTypes>
              <add name="BasicType" type="CA.NGommans.SerializationDemo.Client.SpecializedTypeManager, SerializationDemoClient, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
            </knownTypes>
          </specializedType>
        </behavior>
      </endpointBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>
The above first declares our new extension in the extensions block, then we define our new specializedType block under endpointBehaviors. This is then applied against the client endpoint.

What this does is allow for an inherited type (helper or otherwise) to be substituted on the client side. We do this with a class such as the following:

using System.Runtime.Serialization;
using CA.NGommans.SerializationDemo.Client.BasicServiceReference;

namespace CA.NGommans.SerializationDemo.Client
{
    /// <summary>
    /// Extension type for <see cref="BasicType"/> which is our "Mine" type.
    /// </summary>
    /// <remarks>
    /// Impersonates <see cref="BasicType"/> for the purpose of serialization.
    /// </remarks>
    [DataContract(Name = "BasicType", Namespace = "http://ngommans.ca/SerializationDemo")]
    public class MyType : BasicType
    {
        /// <summary>
        /// Descriptor for this type that makes it <see cref="MyType"/>
        /// </summary>
        public const string TypeDescriptor = "Mine";

...

    }
}
Using our new IDataContractSerrogate extension we can now remap the class so on deserialization, MyType is returned if BasicType.Type is equal to "Mine". For this demonstration, to make the code as reusable as possible we use an interface IConvertType to handle all the complicated work for us. The distilled code that's customized for this implementation is as follows:

/// <summary>
/// Type manager for <see cref="BasicType"/>
/// </summary>
public class SpecializedTypeManager : IConvertType
{
    /// <summary>
    /// Checks if this type is supported.
    /// </summary>
    /// <param name="type">Type to anlayse.</param>
    /// <returns>Boolean true if supported.</returns>
    public bool CanConvertType(Type type)
    {
        bool result = false;
        if (type != null)
        {
            result = typeof(BasicType).IsAssignableFrom(type);
        }
        return result;
    }

    /// <summary>
    /// Attemps to convert a known type to another more specilized type
    /// </summary>
    /// <param name="original">Original object to convert.</param>
    /// <returns>Converted, more specilaized object.</returns>
    public object ConvertType(object original)
    {
        object result = original;

        BasicType orig = original as BasicType;
        if (orig != null)
        {
            if (string.Compare(orig.Type, MyType.TypeDescriptor, true) == 0)
            {
                MyType target = new MyType();
                target.StringValue = orig.StringValue;

                result = target;
            }
        }

        return result;
    }
}
The end result is that we can pass in/out objects that extend the BasicType class allowing for the client to have more specialized objects, regardless of if they are part of the service contract or not.

Output of SerializationDemoClient.exe
Test application for IDataContractSerrogate implementation
----------------------------------------------------------
Testing Service - Round trip with BasicType:
----------------------------------------------------------
Results:
ObjectType:CA.NGommans.SerializationDemo.Client.BasicServiceReference.BasicType
                StringValue:    SampleValue-Server
                Type:           Basic
----------------------------------------------------------
Testing Service - Round trip with MyType:
----------------------------------------------------------
Results:
ObjectType:CA.NGommans.SerializationDemo.Client.MyType
                StringValue:    MySpecialObjectType-Server
                Type:           Mine
----------------------------------------------------------

As shown above, when we pass in MyType, it's translated into BasicType for transmission to the server, and when a BasicType with the matching criteria is sent to the client, we have the ability to instead return a MyType instance, without any intervention after the results are passed from the client proxy. This becomes especially useful when large and complex data structures are sent around as this action happens on a per-class basis, making it easy to manage.