次の方法で共有


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.

Comments

  • Anonymous
    June 07, 2009
    Thank you for submitting this cool story - Trackback from DotNetShoutout

  • Anonymous
    June 07, 2009
    Hi, finally an official Microsoft solution letting safe the task to map client to services types! I have been using the famous Attribute based solution, that is not safe in all scenarios. Thank you for this posting that explains easly how use DataContractResolver in a "Coupled" scenario. -Claudio

  • Anonymous
    October 18, 2009
    So, I have an app with dynamic data types. The back end SQL schema is created on the fly, and so the business objects I transfer are not known at compile time. I have worked around it by using datatables, but this doesnt work in Silverlight. I am in control of both end of the wire. I was wondering if this approach would work if the data types were created dynamically ('Dynamic Data Objects' on both sides of the wire? The service would return metadata first so the client could construct a matching data type and then it could resolve it using the resolver? Is there something canned like this in the works? I hate to reinvent the wheel...

  • Anonymous
    October 20, 2009
    The comment has been removed

  • Anonymous
    November 16, 2009
    This is exactly what I need. But while it sounds fine for self-hosting, how would I access the ServiceHost in IIS hosted scenario?

  • Anonymous
    November 16, 2009
    Can we set this resolver by OperationBehaviorAttribute or config file?

  • Anonymous
    November 30, 2009
    The comment has been removed

  • Anonymous
    December 03, 2009
    Hello, Thanks for the good article. One question though; I've implemented a DataContractResolver, and it works great. One problem though, my services returns interfaces. Since I'm not able to set the DataContract attribute on the interfaces, I'm not able to specify a namespace. And because of this, the deserialization fails. We have been using the KnownType attribute, but it doesn't do the trick when we're now starting to use Entity Framework 4.0 with POCO. This is where the DataContractResolver seem to do the job, but it's a pitty we can't find a way to return interfaces... So, do you have any idea how to do this?

  • Anonymous
    December 04, 2009
    As far as DataContractSerializer is concerned, interfaces are just like the type 'object'. Serialization only cares about fields and properties, not methods. And interfaces can only define methods. (Quick note: the one exception to this rule are the special-cased IEnumerable, ICollection, IList, and IDictionary which are treated as collections) So known types will always be used if your return type is an interface. Now, you should still be able to use a DataContractResolver to map the type back and forth on the wire. If you're in need of changing the namespace of the contract on the wire, you could create a "fake" data contract to return instead of your interface, and then use a resolver that resolves any types that implement that interface. You don't need to have your classes derive from the new "fake" contract; everything should just work.

  • Anonymous
    December 09, 2009
    Ok, thanks for the answer. So, in the first place we wanted to get rid of ServiceKnownType, which we were hoping to do by using DataContractResolver. The reason for using ServiceKnownType in the first place was because we return interfaces. So, the conclusion seem to be that this cannot be done, since the DataContractResolver won't work with interfaces. On the other hand, we now managed to get the serialization/deserialization from Entity Framework to work. We had to make a resolver, and also use ServiceKnownType (which was missing).

  • Anonymous
    May 25, 2011
    The comment has been removed

  • Anonymous
    June 02, 2011
    Any tips on getting the client side in silverlight using this - it looks like a different method may need to be employed through attributes?  Any Samples for that?

  • Anonymous
    June 30, 2011
    Unfortunately, this feature hasn't quite made it to silverlight yet, but you should expect to see it in a future version of silverlight.

  • Anonymous
    February 10, 2012
    Do we need to bind this DataContractResolver on both client and host side explicitly, Or does binding on host side is sufficient?

  • Anonymous
    March 14, 2012
    Hi, I seem to be facing an issue with deserializing the dynamic object in silverlight. I am using the generic resolver with my wcf service to resolve the dynamic known types and sending the entity object on a call back channel using duplex service. I get the message at the client end but the entity object is null. Seems like the resolver works for the service end, but there is nothing at the silverlight end to resolve the type of object in the incoming message. how can I overcome this deserializing issue.

  • Anonymous
    April 17, 2012
    @Vivin and Abhaye, yes in general you'll need compatible resolvers on the client side and the server side. Without DataContractResolver on the client side, you'll need to use the default contract name, namespace for that type, but you won't be able to deserialize just any type

  • Anonymous
    December 23, 2015
    Your implementation contains memory leak. stackoverflow.com/.../memory-leak-wcf-with-datacontract-resolver