DataContract Surrogate

The DataContract sample demonstrates how processes like serialization, deserialization, schema export, and schema import can be customized using a data contract surrogate class. This sample shows how to use a surrogate in a client and server scenario where data is serialized and transmitted between a Windows Communication Foundation (WCF) client and service.

Note

The setup procedure and build instructions for this sample are located at the end of this topic.

The sample uses the following service contract:

[ServiceContract(Namespace = "http://Microsoft.ServiceModel.Samples")]
[AllowNonSerializableTypes]
public interface IPersonnelDataService
{
    [OperationContract]
    void AddEmployee(Employee employee);

    [OperationContract]
    Employee GetEmployee(string name);
}

The AddEmployee operation allows users to add data about new employees and the GetEmployee operation supports search for employees based on name.

These operations use the following data type:

[DataContract(Namespace = "http://Microsoft.ServiceModel.Samples")]
class Employee
{
    [DataMember]
    public DateTime dateHired;

    [DataMember]
    public Decimal salary;

    [DataMember]
    public Person person;
}

In the Employee type, the Person class (shown in the following sample code) cannot be serialized by the DataContractSerializer because it is not a valid data contract class.

public class Person
{
    public string firstName;

    public string lastName;

    public int age;

    public Person() { }
}

You can apply the DataContractAttribute attribute to the Person class, but this is not always possible. For example, the Person class can be defined in a separate assembly over which you have no control.

Given this restriction, one way to serialize the Person class is to substitute it with another class that is marked with DataContractAttribute and copy over necessary data to the new class. The objective is to make the Person class appear as a DataContract to the DataContractSerializer. Note that this is one way to serialize non-data contract classes.

The sample logically replaces the Person class with a different class named PersonSurrogated.

[DataContract(Name="Person", Namespace = "http://Microsoft.ServiceModel.Samples")]
public class PersonSurrogated
{
    [DataMember]
    public string FirstName;

    [DataMember]
    public string LastName;

    [DataMember]
    public int Age;
}

The data contract surrogate is used to achieve this replacement. A data contract surrogate is a class that implements IDataContractSurrogate. In this sample, the AllowNonSerializableTypesSurrogate class implements this interface.

In the interface implementation, the first task is to establish a type mapping from Person to PersonSurrogated. This is used both at serialization time as well as at schema export time. This mapping is achieved by implementing the GetDataContractType(Type) method.

public Type GetDataContractType(Type type)
{
    if (typeof(Person).IsAssignableFrom(type))
    {
        return typeof(PersonSurrogated);
    }
    return type;
}

The GetObjectToSerialize(Object, Type) method maps a Person instance to a PersonSurrogated instance during serialization, as shown in the following sample code.

public object GetObjectToSerialize(object obj, Type targetType)
{
    if (obj is Person)
    {
        Person person = (Person)obj;
        PersonSurrogated personSurrogated = new PersonSurrogated();
        personSurrogated.FirstName = person.firstName;
        personSurrogated.LastName = person.lastName;
        personSurrogated.Age = person.age;
        return personSurrogated;
    }
    return obj;
}

The GetDeserializedObject(Object, Type) method provides the reverse mapping for deserialization, as shown in the following sample code.

public object GetDeserializedObject(object obj,
Type targetType)
{
    if (obj is PersonSurrogated)
    {
        PersonSurrogated personSurrogated = (PersonSurrogated)obj;
        Person person = new Person();
        person.firstName = personSurrogated.FirstName;
        person.lastName = personSurrogated.LastName;
        person.age = personSurrogated.Age;
        return person;
    }
    return obj;
}

To map the PersonSurrogated data contract to the existing Person class during schema import, the sample implements the GetReferencedTypeOnImport(String, String, Object) method, as shown in the following sample code.

public Type GetReferencedTypeOnImport(string typeName,
               string typeNamespace, object customData)
{
if (
typeNamespace.Equals("http://schemas.datacontract.org/2004/07/DCSurrogateSample")
)
    {
         if (typeName.Equals("PersonSurrogated"))
        {
             return typeof(Person);
        }
     }
     return null;
}

The following sample code completes the implementation of the IDataContractSurrogate interface.

public System.CodeDom.CodeTypeDeclaration ProcessImportedType(
          System.CodeDom.CodeTypeDeclaration typeDeclaration,
          System.CodeDom.CodeCompileUnit compileUnit)
{
    return typeDeclaration;
}
public object GetCustomDataToExport(Type clrType,
                               Type dataContractType)
{
    return null;
}

public object GetCustomDataToExport(
System.Reflection.MemberInfo memberInfo, Type dataContractType)
{
    return null;
}
public void GetKnownCustomDataTypes(
        KnownTypeCollection customDataTypes)
{
    // It does not matter what we do here.
    throw new NotImplementedException();
}

In this sample, the surrogate is enabled in ServiceModel by an attribute called AllowNonSerializableTypesAttribute. Developers would need to apply this attribute on their service contract as shown on the IPersonnelDataService service contract above. This attribute implements IContractBehavior and sets up the surrogate on operations in its ApplyClientBehavior and ApplyDispatchBehavior methods.

The attribute is not necessary in this case - it is used for demonstration purposes in this sample. Users can alternatively enable a surrogate by manually adding a similar IContractBehavior, IEndpointBehavior or IOperationBehavior using code or using configuration.

The IContractBehavior implementation looks for operations that use DataContract by checking if they have a DataContractSerializerOperationBehavior registered. If they do, it sets the DataContractSurrogate property on that behavior. The following sample code shows how this is done. Setting the surrogate on this operation behavior enables it for serialization and deserialization.

public void ApplyClientBehavior(ContractDescription description, ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime proxy)
{
    foreach (OperationDescription opDesc in description.Operations)
    {
        ApplyDataContractSurrogate(opDesc);
    }
}

public void ApplyDispatchBehavior(ContractDescription description, ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.DispatchRuntime dispatch)
{
    foreach (OperationDescription opDesc in description.Operations)
    {
        ApplyDataContractSurrogate(opDesc);
    }
}

private static void ApplyDataContractSurrogate(OperationDescription description)
{
    DataContractSerializerOperationBehavior dcsOperationBehavior = description.Behaviors.Find<DataContractSerializerOperationBehavior>();
    if (dcsOperationBehavior != null)
    {
        if (dcsOperationBehavior.DataContractSurrogate == null)
            dcsOperationBehavior.DataContractSurrogate = new AllowNonSerializableTypesSurrogate();
    }
}

Additional steps need to be taken to plug in the surrogate for use during metadata generation. One mechanism to do this is to provide an IWsdlExportExtension which is what this sample demonstrates. Another way is to modify the WsdlExporter directly.

The AllowNonSerializableTypesAttribute attribute implements IWsdlExportExtension and IContractBehavior. The extension can be either an IContractBehavior or IEndpointBehavior in this case. Its IWsdlExportExtension.ExportContract method implementation enables the surrogate by adding it to the XsdDataContractExporter used during schema generation for DataContract. The following code snippet shows how to do this.

public void ExportContract(WsdlExporter exporter, WsdlContractConversionContext context)
{
    if (exporter == null)
        throw new ArgumentNullException("exporter");

    object dataContractExporter;
    XsdDataContractExporter xsdDCExporter;
    if (!exporter.State.TryGetValue(typeof(XsdDataContractExporter), out dataContractExporter))
    {
        xsdDCExporter = new XsdDataContractExporter(exporter.GeneratedXmlSchemas);
        exporter.State.Add(typeof(XsdDataContractExporter), xsdDCExporter);
    }
    else
    {
        xsdDCExporter = (XsdDataContractExporter)dataContractExporter;
    }
    if (xsdDCExporter.Options == null)
        xsdDCExporter.Options = new ExportOptions();

    if (xsdDCExporter.Options.DataContractSurrogate == null)
        xsdDCExporter.Options.DataContractSurrogate = new AllowNonSerializableTypesSurrogate();
}

When you run the sample, the client calls AddEmployee followed by a GetEmployee call to check if the first call was successful. The result of the GetEmployee operation request is displayed in the client console window. The GetEmployee operation must succeed in finding the employee and print "found".

Note

This sample shows how to plug in a surrogate for serialize, deserialize and metadata generation. It does not show how to plug in a surrogate for code generation from metadata. To see a sample of how a surrogate can be used to plug into client code generation, see the Custom WSDL Publication sample.

To set up, build, and run the sample

  1. Ensure that you have performed the One-Time Setup Procedure for the Windows Communication Foundation Samples.

  2. To build the C# edition of the solution, follow the instructions in Building the Windows Communication Foundation Samples.

  3. To run the sample in a single- or cross-machine configuration, follow the instructions in Running the Windows Communication Foundation Samples.