Configuring Known Types Dynamically - Introducing the DataContractResolver

This post has been updated to account for the changes to the DataContractResolver in the Beta 2 release of .NET 4.0.

 

This will be my first of several posts about new features that are showing up in the Beta 1 release of .NET 4.0. Beta 1 was released just a few weeks ago and you can download it today here. The most important addition to DataContract Serialization in this release is the addition of the DataContractResolver.

In previous posts, I’ve talked about known types: why they’re needed in some situations and how to use them. Up to now though, it’s only been possible to define known types statically. That is, you have to know ahead of time what types you want to register as known types. So in previous examples, you had to know that Dog would be a known type and then register it as such. This is a problem though if you don’t know the types ahead of time. What if you wanted all types to be serialized out to the wire, regardless of whether they were listed as known types? In that case, you would need a callback mechanism to serialize known types and then resolve them on deserialization. That is exactly what DataContractResolver can do for you. Users can inherit from DataContractResolver, implement the callbacks, and then plug their resolvers into a serializer or into WCF.

But first let’s go through the basics of how the resolver works:

DataContractResolver defines two methods that you can override:

bool TryResolveType(Type dataContractType, Type declaredType, DataContractResolver knownTypeResolver, out XmlDictionaryString typeName, out XmlDictionaryString typeNamespace)

Type ResolveName(string typeName, string typeNamespace, Type declaredType, DataContractResolver knownTypeResolver)

TryResolveType is called on serialization to map a type into the name and namespace used to define the xsi:type, while ResolveName is called on deserialization to map the xsi:type name and namespace back into a type.

TryResolveType takes the type of the object being serialized, a declared type (more on these later), and a DataContractResolver. That resolver is the known type DataContractResolver. It mimics the behavior of the static known type resolution logic. So you can always call the known type resolver to get the same behavior you would have gotten without your resolver. The TryResolveType method returns a boolean indicating whether the resolver is able to resolve the type that was passed in, and two XmlDictionaryStrings which will be used to write the resolved name and namespace on the wire. If you’d rather deal with strings instead, it’s very easy to go from strings to XmlDictionaryStrings. Simply create an XmlDictionary and use the return value of the Add method:

XmlDictionary dictionary = new XmlDictionary();

typeName = dictionary.Add("string");

ResolveName also takes a declared type and a known type resolver, but it works the opposite way. It takes two strings which represent the name and namespace and returns a type.

To demonstrate how this class works, let’s play around with a couple types:

[DataContract]

public class Animal

{

    [DataMember]

    public int age = 4;

    [DataMember]

    public string name = "Rusty";

}

[DataContract]

public class Dog : Animal

{

    [DataMember]

    public DogBreed breed = DogBreed.LabradorRetriever;

}

public enum DogBreed {

    GermanShepherd,

    LabradorRetriever

}

Notice that I specifically haven’t referenced Dog as a known type. So if you try the following right now:

XmlObjectSerializer serializer = new DataContractSerializer(typeof(Animal));

serializer.WriteObject(new XmlTextWriter(Console.Out) { Formatting = Formatting.Indented }, new Dog());

You get the familiar known type exception string with a twist:

Unhandled Exception: System.Runtime.Serialization.SerializationException: Type 'Serialization.Dog' with data contract name 'Dog:https://schemas.datacontract.org/2004/07/Serialization' is not expected. Consider using a DataContractResolver or add any types not known statically to the list of known types - for example, by using the KnownTypeAttribute attribute or by adding them to the list of known types passed to DataContractSerializer.

Since the exception message told us to, let’s go ahead and do just that. I’m going to implement a simple resolver:

public class DogResolver : DataContractResolver

{

    public override bool TryResolveType(Type dataContractType, Type declaredType, DataContractResolver knownTypeResolver, out XmlDictionaryString typeName, out XmlDictionaryString typeNamespace)

    {

        if (dataContractType == typeof(Dog))

        {

            XmlDictionary dictionary = new XmlDictionary();

            typeName = dictionary.Add("WOOF");

            typeNamespace = dictionary.Add("https://www.myAnimals.com");

            return true; // indicating that this resolver knows how to handle "Dog"

        }

        else

        {

   // Defer to the known type resolver

           return knownTypeResolver.TryResolveType(dataContractType, declaredType, null, out typeName, out typeNamespace);

        }

    }

    public override Type ResolveName(string typeName, string typeNamespace, DataContractResolver knownTypeResolver)

    {

        if (typeName == "WOOF" && typeNamespace == "https://www.myAnimals.com")

        {

            return typeof(Dog);

        }

        else

        {

    // Defer to the known type resolver

            return knownTypeResolver.ResolveName(typeName, typeNamespace, null);

        }

    }

}

This implementation essentially says “Map Dog to https://www.myAnimals.com:WOOF and map https://www.myAnimals.com:WOOF back to Dog”. When you don’t know how to handle a type, you should defer to the known type resolver. Now if you try the following code, plugging in your custom resolver in the DataContractSerializer constructor:

XmlObjectSerializer serializer = new DataContractSerializer(typeof(Animal), null, Int32.MaxValue, false, false, null, new DogResolver());

serializer.WriteObject(new XmlTextWriter(Console.Out) { Formatting = Formatting.Indented }, new Dog());

Not only do you not get an exception, but you should get the following XML:

<Animal xmlns:i="https://www.w3.org/2001/XMLSchema-instance" xmlns:d1p1="https://www.myAnimals.com" i:type="d1p1:WOOF" xmlns="https://schemas.datacontract.org/2004/07/Serialization">

  <age>4</age>

  <name>Rusty</name>

  <breed>LabradorRetriever</breed>

</Animal>

Notice how the type showed up on the wire with xsi:type = https://www.myAnimals.com:WOOF.

It’s a good thing that serialization works, but your deserialization roundtrip works as well:

MemoryStream ms = new MemoryStream();

XmlObjectSerializer serializer = new DataContractSerializer(typeof(Animal), null, Int32.MaxValue, false, false, null, new DogResolver());

serializer.WriteObject(ms, new Dog());

ms.Position = 0;

Console.WriteLine(((Dog)serializer.ReadObject(ms)).breed);

Should print out LabradorRetriever.

The Declared Type parameter

 

As an input to both DataContractResolver methods, you’ll find a parameter called “declaredType”. What this corresponds to is the type that the serializer “expects” based on the contract. So, above, the serializer expected an Animal but got a Dog instead. So the declaredType for both methods would be the Animal type. Declared types are useful to have because they allow you to create a resolver like this:

 

public class DeserializeAsBaseResolver : DataContractResolver

{

    public override bool TryResolveType(Type type, Type declaredType, DataContractResolver knownTypeResolver, out XmlDictionaryString typeName, out XmlDictionaryString typeNamespace)

    {

        return knownTypeResolver.TryResolveType(type, declaredType, null, out typeName, out typeNamespace);

    }

    public override Type ResolveName(string typeName, string typeNamespace, Type declaredType, DataContractResolver knownTypeResolver)

    {

        return knownTypeResolver.ResolveName(typeName, typeNamespace, declaredType, null) ?? declaredType;

    }

}

 

This resolver first calls the knownTypeResolver, and if that fails, allows you to always deserialize as the type of the contract, effectively disregarding the xsi:type name on the wire. This is useful in a situation where you can receive any number of types that derive from one base class, but all you care about are the members on the base class. The resolver above would allow you to deserialize all instances of derived classes as the base class itself.

 

How to register a DataContractResolver

You’ve already seen how to plug in a DataContractResolver in the serializer’s constructor, but there are two other ways of registering your custom DataContractResolver:

You can plug it in on DataContractSerializer’s ReadObject and WriteObject methods:

MemoryStream ms = new MemoryStream();

DataContractSerializer serializer = new DataContractSerializer(typeof(Animal));

XmlDictionaryWriter writer = XmlDictionaryWriter.CreateDictionaryWriter(XmlWriter.Create(ms));

serializer.WriteObject(writer, new Dog(), new DogResolver());

writer.Flush();

ms.Position = 0;

Console.WriteLine(((Dog)serializer.ReadObject(XmlDictionaryReader.CreateDictionaryReader(XmlReader.Create(ms)), false, new DogResolver())).breed);

Note that this overload currently only works for XmlDictionaryReader/XmlDictionaryWriter and not for Stream or XmlReader/Writer.

Finally, you can also plug it into WCF by setting it on the DataContractSerializerOperationBehavior:

ServiceHost host = new ServiceHost(typeof(MyService));

ContractDescription cd = host.Description.Endpoints[0].Contract;

OperationDescription myOperationDescription = cd.Operations.Find("Echo");

DataContractSerializerOperationBehavior serializerBehavior = myOperationDescription.Behaviors.Find<DataContractSerializerOperationBehavior>();

if (serializerBehavior == null)

{

    serializerBehavior = new DataContractSerializerOperationBehavior(myOperationDescription);

    myOperationDescription.Behaviors.Add(serializerBehavior);

}

serializerBehavior.DataContractResolver = new DogResolver();

host.Open();

Console.WriteLine("Service started successfully");

Console.ReadKey();

host.Close();

Useful resolvers

Hopefully, you can see the use of the DataContractResolver. But if you still need help with that, here are a couple of resolvers you may find useful:

public class SharedTypeResolver : DataContractResolver

{

    public override bool TryResolveType(Type dataContractType, Type declaredType, DataContractResolver knownTypeResolver, out XmlDictionaryString typeName, out XmlDictionaryString typeNamespace)

    {

        if (!knownTypeResolver.TryResolveType(dataContractType, declaredType, null, out typeName, out typeNamespace))

        {

            XmlDictionary dictionary = new XmlDictionary();

        typeName = dictionary.Add(dataContractType.FullName);

            typeNamespace = dictionary.Add(dataContractType.Assembly.FullName);

        }

        return true;

    }

    public override Type ResolveName(string typeName, string typeNamespace, Type declaredType, DataContractResolver knownTypeResolver)

    {

        return knownTypeResolver.ResolveName(typeName, typeNamespace, declaredType, null) ?? Type.GetType(typeName + ", " + typeNamespace);

    }

}

 

This resolver gives the knownTypeResolver a chance to resolve a type or a name statically, and if that fails it uses the type and assembly name of the type on the wire instead. Note that this would only work on the .NET framework for tightly coupled scenarios. You’re basically getting DataContractSerializer behavior most of the type, and NetDataContractSerializer behavior for known types without ever having to specify a known type yourself.

For our last resolver, here’s something a bit more involved:

public class CachingResolver : DataContractResolver

{

    Dictionary<string, int> serializationDictionary;

    Dictionary<int, string> deserializationDictionary;

    int serializationIndex = 0;

    XmlDictionary dic;

    public CachingResolver()

    {

        serializationDictionary = new Dictionary<string, int>();

        deserializationDictionary = new Dictionary<int, string>();

        dic = new XmlDictionary();

    }

    public override bool TryResolveType(Type dataContractType, Type declaredType, DataContractResolver knownTypeResolver, out XmlDictionaryString typeName, out XmlDictionaryString typeNamespace)

    {

        if (!knownTypeResolver.TryResolveType(dataContractType, declaredType, null, out typeName, out typeNamespace))

        {

            return false;

        }

        int index;

        if (serializationDictionary.TryGetValue(typeNamespace.Value, out index))

        {

            typeNamespace = dic.Add(index.ToString());

        }

        else

        {

            serializationDictionary.Add(typeNamespace.Value, serializationIndex);

            typeNamespace = dic.Add(serializationIndex++ + "#" + typeNamespace);

        }

        return true;

    }

    public override Type ResolveName(string typeName, string typeNamespace, Type declaredType, DataContractResolver knownTypeResolver)

    {

        Type type;

        int deserializationIndex;

        int poundIndex = typeNamespace.IndexOf("#");

        if (poundIndex < 0)

        {

            if (Int32.TryParse(typeNamespace, out deserializationIndex))

            {

                deserializationDictionary.TryGetValue(deserializationIndex, out typeNamespace);

            }

            type = knownTypeResolver.ResolveName(typeName, typeNamespace, declaredType, null);

        }

        else

        {

            if (Int32.TryParse(typeNamespace.Substring(0, poundIndex), out deserializationIndex))

            {

                typeNamespace = typeNamespace.Substring(poundIndex + 1, typeNamespace.Length - poundIndex - 1);

                deserializationDictionary.Add(deserializationIndex, typeNamespace);

            }

            type = knownTypeResolver.ResolveName(typeName, typeNamespace, declaredType, null);

        }

        return type;

    }

}

 

This resolver allows you to cache namespaces. Since namespaces tend to be reused a lot and are usually very long, this improves sizes of known type namespaces by giving them a caching index: 0, 1, 2, …. It defines a namespace by using the # sign, so for example: "0#https://schemas.datacontract.org/2004/07/Serialization" and then whenever that namespace is reused in the future, it simply uses “0” as the namespace. The deserializer recognizes this syntax and maintains a dictionary of indices to namespaces.

Note however that this resolver makes several assumptions: notably that the serializer only talks to one deserializer and that instances are received in order.

So, as you can see, there are multiple uses for the DataContractResolver. But the main idea is the following: dynamic known type resolution. Hopefully this feature will make known types more powerful and a little bit less confusing to use.

You can find the full Beta 2 MSDN documentation for the DataContractResolver class at: https://msdn.microsoft.com/en-us/library/system.runtime.serialization.datacontractresolver(VS.100).aspx.