WCF: Returning a derived object for a contract with base object (DataContractResolver)
Asked Answered
A

2

1

I have have a WCF derived/base contract issue. I have a server interface/contract that returns a BaseThing object:

[OperationContract]
BaseThing Get_base_thing();

The server that implements this has a DerivedThing (derived from BaseThing) and wants to return this as a BaseThing. How to tell WCF that I only want to transport the BaseThing part of DerivedThing?

If in Get_base_thing I just return a reference to a DerivedThing then I get a SerializationException server side.

I think I need to define a DataContractResolver and I looked at the MSDN article Using a Data Contract Resolver but this is not 100% clear (to me a least).

How should my DataContractResolver look to tell WCF to only transport the base part of the derived object I pass it?

Is there some way to do this more simply just with KnownType attribue?

Anguiano answered 6/12, 2011 at 15:22 Comment(0)
A
1

After posting I also found this SO identical question How to serialize a derived type as base. The unaccepted second answer by marc for me is the easiest way to resolve this issue. That is:
Decorate the derived class with [DataContract(Name="BaseClass")]
Note that this solution means that derived will transport as base for all every case of transport of this object. For me that was not an issue if it is then you need to go the DataContractResolver route.

Some notes on the DataContractResolver route:
1. This enables you to pass the derived as derived on some calls but as base on other - if you need to do that - if not use about Name= approach.
2. I get an exception using the DeserializeAsBaseResolver from the datacontractrsolver article as it stands because the knownTypeResolver returns false. To fix that I ignor the return value of that call and always return true from TryResolveType. That seems to work.
3. I initially thought that because we were serializing as base that I didnt need [DataContract] on the derived class. That was wrong. The object is serialized as the derived object and derserialized as a base object - so you must decorate the derived with [DataContract] but don't mark any fields as [DataMembers] to avoid them being unnecessarily serialize.
4. If you have a command line host and a service host then you need the code to insert the contract resolver in both. I found it useful to put this as a static in my resolver.
5. Note that that the "Get_gateway_data" string in the call to cd.Operations.Find("Get_gateway_data") is the name of the contract method that returns the object concerned. You will need to do this for each call that you want this behaviour.

Final code for this approach:

public class DeserializeAsBaseResolver : DataContractResolver {

    public static void Install(ServiceHost service_host) {
        // Setup DataContractResolver for GatewayProcessing to GatewayData resolution:
        ContractDescription cd = service_host.Description.Endpoints[0].Contract;
        OperationDescription myOperationDescription = cd.Operations.Find("Get_gateway_data");
        DataContractSerializerOperationBehavior serializerBehavior = myOperationDescription.Behaviors.Find<DataContractSerializerOperationBehavior>();
        if (serializerBehavior == null) {
            serializerBehavior = new DataContractSerializerOperationBehavior(myOperationDescription);
            myOperationDescription.Behaviors.Add(serializerBehavior);
        }
        serializerBehavior.DataContractResolver = new DeserializeAsBaseResolver();
    }

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

        bool ret = knownTypeResolver.TryResolveType(type, declaredType, null, out typeName, out typeNamespace);
        //return ret; // ret = false which causes an exception.
        return true;
    }

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

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

Host code (service or command line):

using (ServiceHost service_host = new ServiceHost(typeof(GatewayServer))) {

// Setup DataContractResolver for GatewayProcessing to GatewayData resolution:
DeserializeAsBaseResolver.Install(service_host);

// Open the host and start listening for incoming messages.
try { service_host.Open(); }
Anguiano answered 7/12, 2011 at 11:12 Comment(0)
L
3

KnownType will not resolve this issue.

It sounds as if you have a serious divergence between the object model you're using at the server and the service contracts you're using. There seems to be 3 possible solutions:

1) Data Contract Resolver as you've identified to make it automatic across all your operations. There are a number of examples out there including this one: http://blogs.msdn.com/b/youssefm/archive/2009/06/05/introducing-a-new-datacontractserializer-feature-the-datacontractresolver.aspx.

2) Align your object model to better match your service contracts. That is, use containment rather than inheritance to manage the BaseThing-DerivedThing relationship. That way you work with DerivedThing at the server and simply return DerivedThing.BaseThing over the wire. If BaseThing needs to get transmitted from client to server, this will also work better.

3) Use explicit conversion using something like AutoMapper so it is obvious in your operations that there is a divergence between the objects being used at the server and those exposed to the outside world.

Lytta answered 6/12, 2011 at 23:0 Comment(1)
I've changed to accept my own answer below because I want to emphasise the Name="BaseClass" apprach which is really what I was looking for. Thanks for your input. +1 For KnownType will not resolve.Anguiano
A
1

After posting I also found this SO identical question How to serialize a derived type as base. The unaccepted second answer by marc for me is the easiest way to resolve this issue. That is:
Decorate the derived class with [DataContract(Name="BaseClass")]
Note that this solution means that derived will transport as base for all every case of transport of this object. For me that was not an issue if it is then you need to go the DataContractResolver route.

Some notes on the DataContractResolver route:
1. This enables you to pass the derived as derived on some calls but as base on other - if you need to do that - if not use about Name= approach.
2. I get an exception using the DeserializeAsBaseResolver from the datacontractrsolver article as it stands because the knownTypeResolver returns false. To fix that I ignor the return value of that call and always return true from TryResolveType. That seems to work.
3. I initially thought that because we were serializing as base that I didnt need [DataContract] on the derived class. That was wrong. The object is serialized as the derived object and derserialized as a base object - so you must decorate the derived with [DataContract] but don't mark any fields as [DataMembers] to avoid them being unnecessarily serialize.
4. If you have a command line host and a service host then you need the code to insert the contract resolver in both. I found it useful to put this as a static in my resolver.
5. Note that that the "Get_gateway_data" string in the call to cd.Operations.Find("Get_gateway_data") is the name of the contract method that returns the object concerned. You will need to do this for each call that you want this behaviour.

Final code for this approach:

public class DeserializeAsBaseResolver : DataContractResolver {

    public static void Install(ServiceHost service_host) {
        // Setup DataContractResolver for GatewayProcessing to GatewayData resolution:
        ContractDescription cd = service_host.Description.Endpoints[0].Contract;
        OperationDescription myOperationDescription = cd.Operations.Find("Get_gateway_data");
        DataContractSerializerOperationBehavior serializerBehavior = myOperationDescription.Behaviors.Find<DataContractSerializerOperationBehavior>();
        if (serializerBehavior == null) {
            serializerBehavior = new DataContractSerializerOperationBehavior(myOperationDescription);
            myOperationDescription.Behaviors.Add(serializerBehavior);
        }
        serializerBehavior.DataContractResolver = new DeserializeAsBaseResolver();
    }

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

        bool ret = knownTypeResolver.TryResolveType(type, declaredType, null, out typeName, out typeNamespace);
        //return ret; // ret = false which causes an exception.
        return true;
    }

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

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

Host code (service or command line):

using (ServiceHost service_host = new ServiceHost(typeof(GatewayServer))) {

// Setup DataContractResolver for GatewayProcessing to GatewayData resolution:
DeserializeAsBaseResolver.Install(service_host);

// Open the host and start listening for incoming messages.
try { service_host.Open(); }
Anguiano answered 7/12, 2011 at 11:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.