If you have coded some in WCF, you will sooner or later have experienced the issue where you want to derive a data contract and create a WCF service with an operation that returns a base class, but the WCF service in fact can return derived classes. In any case, when you use inheritance with data contracts, there are issues around the serialization and deserialization. The solution for now has been to decorate the base class with the
KnownTypeAttribute of all the derived classes that should be supported. Clearly, this strategy only works in the simplest scenarios and sooner or later you will have runtime exceptions while serializing and deserializing as the WCF service operations execute.
A solution to this, is to create a custom
DataContractResolver. This custom object will inherit from the
DataContractResolver and customize the serialization and deserialization process. For this to work, you will have to set the
DataContractResolver on WCF operations and make the necessary adjustments both on the serverside and the clientside.
I have created a custom ServiceHostFactory that sets the
DataContractResolver for all WCF operations for all endpoints. For some scenarios, this is not desired, but the source code shows how we can globally apply this
DataContractResolver to all WCF operations.
The ServiceHostFactory is called WebEnabledServiceHostFactory and looks like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Description;
using System.Web;
namespace TestServiceHostFactory.IIS
{
public class WebEnabledServiceHostFactory : ServiceHostFactory
{
private static readonly object _locker = new object();
private static Dictionary _serviceLookup = new Dictionary();
private static readonly string _dataContractAssemblyName = "Types";
private static Assembly _dataContractAssembly;
private static AssemblyAwareDataContractResolver _dataContractResolver;
static WebEnabledServiceHostFactory()
{
lock (_locker)
{
_serviceLookup.Add(typeof(ICalculatorService), typeof(CalculatorService));
_serviceLookup.Add(typeof(ICustomerService), typeof(CustomerService));
}
}
protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
{
ServiceHost host = new ServiceHost(_serviceLookup[serviceType], baseAddresses);
ServiceEndpoint endpoint = host.AddServiceEndpoint(serviceType, new WebHttpBinding(), "web");
ServiceEndpoint endpointWs = host.AddServiceEndpoint(serviceType, new WSHttpBinding(), "ws");
endpoint.EndpointBehaviors.Add(new WebHttpBehavior());
ServiceMetadataBehavior smb = new ServiceMetadataBehavior { HttpGetEnabled = true };
host.Description.Behaviors.Add(smb);
foreach (ServiceEndpoint serviceEndpoint in host.Description.Endpoints)
{
foreach (OperationDescription operation in serviceEndpoint.Contract.Operations)
{
DataContractSerializerOperationBehavior serializerBehavior = operation.OperationBehaviors
.OfType<DataContractSerializerOperationBehavior>().FirstOrDefault();
if (serializerBehavior == null)
{
serializerBehavior = new DataContractSerializerOperationBehavior(operation);
operation.OperationBehaviors.Add(serializerBehavior);
}
if (_dataContractAssembly == null)
_dataContractAssembly = Assembly.Load(_dataContractAssemblyName);
if (_dataContractResolver == null)
_dataContractResolver = new AssemblyAwareDataContractResolver(_dataContractAssembly);
serializerBehavior.DataContractResolver = _dataContractResolver;
}
}
return host;
}
}
}
This WCF ServiceHostFactory derived class both supports web operations and sets a custom DataContractResolver,
AssemblyAwareDataContractResolver.
This data contract resolver is assembly aware, i.e. it can traverse an assembly, look for all types (classes), decorated with the
DataContract attribute, signifying that this class is a data contract.
As you can see in the ServiceHostFactory code above, we loop through all the WCF endpoints, then loop through all the operations (OperationDescription) inside the Contract property of the Endpoint, next we look at the
OperationBehaviors if there is already added a
DataContractSerializerOperationBehavior and if not, we add such an object. We finally specify the
DataContractResolver of this behavior which we set to our custom
AssemblyAwareDataContractResolver. Note that we add the assembly in the constructor to this object. We know in this special case that the assembly is in the
Types project. An alternative would be to expose a public property for example to set the name of the assembly (i.e. project), where the assembly resides. Another alternative would be to support multiple assemblies where data contracts will reside. For most cases, most developers has got their data contracts set up in a
Common project and assembly, along with
Service Contracts.
Let's look at the source code of
AssemblyAwareDataContractResolver next:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Web;
using System.Xml;
using System.Collections.Concurrent;
using System.Text;
using System.Diagnostics;
using System.IO;
namespace TestServiceHostFactory.IIS
{
public class AssemblyAwareDataContractResolver : DataContractResolver
{
private ConcurrentDictionary dictionary = new ConcurrentDictionary();
private Assembly assembly;
public AssemblyAwareDataContractResolver(Assembly assembly)
{
this.assembly = assembly;
FamiliarizeWithDataContracts(assembly);
}
private void FamiliarizeWithDataContracts(Assembly assembly)
{
DataContractSerializer serializer = new DataContractSerializer(typeof(Object), null, int.MaxValue, false, true, null,
this);
StringBuilder stringBuilder = new StringBuilder();
foreach (Type type in assembly.GetTypes())
{
SerializeDataType(serializer, stringBuilder, type);
}
//foreach (Type type in assembly.GetTypes())
//{
// SerializeDataType(serializer, stringBuilder, type);
// DeserializeDataType(serializer, stringBuilder);
//}
}
//private static void DeserializeDataType(DataContractSerializer serializer, StringBuilder stringBuilder)
//{
// string serialized = stringBuilder.ToString();
// using (XmlReader xmlReader = XmlReader.Create(new StringReader(serialized)))
// {
// object deSerialized = serializer.ReadObject(xmlReader);
// }
//}
private static void SerializeDataType(DataContractSerializer serializer, StringBuilder stringBuilder, Type type)
{
if (type.GetCustomAttributes().Count() > 0)
{
var dataContract = Activator.CreateInstance(type);
using (XmlWriter xmlWriter = XmlWriter.Create(stringBuilder))
{
try
{
serializer.WriteObject(xmlWriter, dataContract);
}
catch (SerializationException se)
{
Debug.WriteLine(se.Message);
}
}
Debug.WriteLine(stringBuilder.ToString());
}
}
///
/// Serialization will use this overload
///
public override bool TryResolveType(Type type, Type declaredType, DataContractResolver knownTypeResolver, out XmlDictionaryString typeName, out XmlDictionaryString typeNamespace)
{
string name = type.Name;
string namesp = type.Namespace;
typeName = new XmlDictionaryString(XmlDictionary.Empty, name, 0);
typeNamespace = new XmlDictionaryString(XmlDictionary.Empty, namesp, 0);
if (!dictionary.ContainsKey(type.Name))
dictionary.TryAdd(name, typeName);
if (!dictionary.ContainsKey(type.Namespace))
dictionary.TryAdd(namesp, typeNamespace);
return true;
}
///
/// Deserialization will use this overload
///
public override Type ResolveName(string typeName, string typeNamespace, Type declaredType, DataContractResolver knownTypeResolver)
{
XmlDictionaryString tName;
XmlDictionaryString tNamespace;
if (dictionary.TryGetValue(typeName, out tName) && dictionary.TryGetValue(typeNamespace, out tNamespace))
return this.assembly.GetType(tNamespace.Value + "." + tName.Value);
else
return null;
}
}
}
Make note that we
familiarize the AssemblyAwareDataContractResolver by looping through all types and serialize them (default types and classes), using
Activator.CreateInstance and serialize the object. This is done to set up a dictionary data structure (from the Concurrent assembly). Of course, this will have some overhead, which is why we use the same AssemblyAwareDataContractResolver instance in the ServiceHostFactory for quick serialization and deserialization, once inited.
You can also see that we derive from the DataContractResolver class and override ResolveName (used during deserialization) and TryResolveType (used during serialization).
Next, we need some service to test out WCF with these new capabilities. Lets first look at the data contracts involved in the
Types project (class library):
using System.Runtime.Serialization;
namespace Types
{
[DataContract]
public class Customer
{
[DataMember]
public string Name { get; set; }
}
[DataContract]
public class RegularCustomer : Customer
{
}
[DataContract]
public class VIPCustomer : Customer
{
[DataMember]
public string VipInfo { get; set; }
[DataMember]
public Customer Attache { get; set; }
}
[DataContract]
public class PreferredVIPCustomer : VIPCustomer
{
}
}
Make note we have inheritance here of our data contracts, and that we have not set the
KnownType attribute on the base class for the derived classes. After all, we want our WCF service to be
assembly aware, right?
Let's define a service contract next. This resides in the WCF service application, but should be moved to the
Types project.
using System.ServiceModel;
using System.ServiceModel.Web;
using Types;
namespace TestServiceHostFactory.IIS
{
[ServiceContract(Namespace = "http://www.foo.bar.com")]
public interface ICustomerService
{
[OperationContract]
[WebGet]
Customer GetCustomer(string name);
}
}
The implementation is in the same project, CustomerService:
using Types;
namespace TestServiceHostFactory.IIS
{
public class CustomerService : ICustomerService
{
public Customer GetCustomer(string name)
{
if (!name.Contains("VIP"))
return new Customer { Name = name };
else
{
var result = new VIPCustomer { Name = name, VipInfo = "VIP: " + name, Attache = new Customer { Name = "Geoffrey" } };
return result;
}
}
}
}
We also need a Customer.svc file to expose our service:
<% @ ServiceHost Language="C#" Service="TestServiceHostFactory.IIS.ICustomerService" Factory="TestServiceHostFactory.IIS.WebEnabledServiceHostFactory" %>
The <system.serviceModel> section of the web config file in this example is empty, as the complexity of setting up endpoints and behaviors are
taken care of in the custom service host.
The next step is to connect to this WCF
CustomerService and check that the automatic resolution of inheritance in the data contracts of the assembly
Types is taken care of with our custom
(AssemblyAware)DataContractResolver. I have created a simple console project with a main method inside where I put the client initialization code and the set up a channel factory to communicate with the service. I know the endpoint to go against.
using System;
using System.Linq;
using System.Reflection;
using System.ServiceModel;
using System.ServiceModel.Description;
using TestServiceHostFactory.IIS;
namespace TestServiceHostFactory.Client
{
class Program
{
static void Main(string[] args)
{
WSHttpBinding binding = new WSHttpBinding();
EndpointAddress endpoint = new EndpointAddress("http://localhost:51229/Customer.svc/ws");
ChannelFactory customerService = new ChannelFactory(binding, endpoint);
Assembly dataContractAssembly = Assembly.Load("Types");
foreach (var operationDescription in customerService.Endpoint.Contract.Operations)
{
var serializerBehavior = operationDescription.OperationBehaviors.OfType<DataContractSerializerOperationBehavior>().FirstOrDefault();
if (serializerBehavior == null)
{
serializerBehavior = new DataContractSerializerOperationBehavior(operationDescription);
operationDescription.OperationBehaviors.Add(serializerBehavior);
}
serializerBehavior.DataContractResolver = new AssemblyAwareDataContractResolver(dataContractAssembly);
}
ICustomerService c = customerService.CreateChannel();
var cust = c.GetCustomer("VIP Tore Aurstad");
Console.WriteLine("Press any key to continue ...");
Console.ReadKey();
}
}
}
I set up a binding instance of type WsHttpBinding (since this must match the .AddServiceEndPoint calls defined on the ServiceHost on the server side, which is performed inside the WebEnabledServiceHostFactory), and create an Endpoint with an address, which matches the address set up of the Service Application project set up. To see this url, choose a WCF service application project, right click and select properties and click on the Web tab pane and look at the
Project Url, where the port number should be described.
I create a new ChannelFactory to go against the
ICustomerService WCF service (interface). Also note that the client side must have set up its corresponding AssemblyAwareDataContractResolver.
You will want to reuse the same object for and not instantiate it as I have done here, for efficiency reasons, after all - remember the FamiliarizeWithDataContract call. For this demonstration, I have not done this optimization. We then use the
CreateChannel method of the
ChannelFactory and create the connection between the client and the server. Now we can call the WCF operation GetCustomer. We pass in the string "VIP Tore Aurstad", and if you look at the logic, this will return a
VIPCustomer object. This is a derived class of the
Customer data contract type. Although we have not specified
KnownType attribute, we still avoid runtime crash, since our DataContractResolver is now
assembly aware.
To sum up, from .NET Framework version 4.0, WCF offers you to take control of how DataContracts are resolved during serialization and deserialization. If you have a project (class library) with many data contracts, you should consider using a class hierarchy to reduce the number of classes used for data contracts. Many developers avoid inheritance in data contracts that are used with WCF, because of the costs around maintaining the KnownType attributes. In fact, many developers are not sure what to do when they encounter these runtime exceptions in the first place. By taking your time to implement an assembly aware DataContractResolver, you are reducing a maintenance cost around adding KnownType to your data contracts.
Of course, this means you must have an efficient implementation of this. Note what I said about the client code needs to keep a reference to the instance of
AssemblyAwareDataContractResolver when you loop through endpoints and operations. For this simple implementation, this issue did not matter.
It is expected that the FamiliarizeWithDataContract method is quick enough, but you should consider parallelizing it further to register the serializing data ccontracts. If you extract the client code above into a ChannelManager or similar, you can cache this object into a static variable, such that the overhead is mminimal. This cost compared to the reduced maintenance cost of inheritance and
KnownType should be worth the effort. In addition, by better supporting inheritance with data contracts, you should be able to design better class hierarchies for your data contracts and implement DRY in your data contracts.