Deserialize derived types in WCF service as base types but retain type information
Asked Answered
B

1

8

I want my service to be able to accept and return types derived from BaseType without actually knowing what those types will be. I have almost got a solution using a custom DataContractResolver based on the SharedTypeResolver from this excellent blog post.

The missing piece of the puzzle is that the types my service will handle might not be shared and known to the service but I still want to accept them and be aware of what the type should have been. I have come up with the following example of a service that acts as a stack. You can push and pop any type derived from BaseType provided you use the SharedTypeResolver and the types are shared between client and server.

[DataContract]
public class BaseType
{
    [DataMember]
    public string SomeText { get; set; }

    public override string ToString()
    {
        return this.GetType().Name + ": " + this.SomeText;
    }
}

[DataContract]
public class DerivedType : BaseType
{
    [DataMember]
    public int SomeNumber { get; set; }

    public override string ToString()
    {
        return base.ToString() + ", " + this.SomeNumber;
    }
}

[ServiceContract]
public interface ITypeStack
{
    [OperationContract]
    void Push(BaseType item);

    [OperationContract]
    BaseType Pop();
}

[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
public class TypeStackService : ITypeStack
{
    private Stack<BaseType> stack = new Stack<BaseType>();

    public void Push(BaseType item)
    {
        this.stack.Push(item);
    }

    public BaseType Pop()
    {
        return this.stack.Pop();
    }
}

This is obviously greatly a simplified example of the problem I'm having. A client can quite merrily push and pop BaseType or DerivedType because both client and server know about them, but if the client pushes UnsharedType which the service does not know I get an error as you would expect.

The formatter threw an exception while trying to deserialize the message: There was an error while trying to deserialize parameter http://tempuri.org/:item. The InnerException message was 'Error in line 1 position 316. Element 'http://tempuri.org/:item' contains data from a type that maps to the name 'TestWcfClient, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null:TestWcfClient.UnsharedType'. The deserializer has no knowledge of any type that maps to this name. Consider changing the implementation of the ResolveName method on your DataContractResolver to return a non-null value for name 'TestWcfClient.UnsharedType' and namespace 'TestWcfClient, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'.'. Please see InnerException for more details.

My current thinking is to add IExtensibleDataObject to BaseType to hold the values from an unshared type and make an unshared type look like BaseType to the service on deserialization when an item is pushed; the opposite would need to happen when an item is popped. I'm just not sure how to go about it. My thoughts so far on possible approaches:

  • Further customisation to DataContractResolver which might involve a TypeDelegator
  • Using IDataContractSurrogate in place of an unshared type
  • Somehow retain the serialized XML the service received when an item is pushed, then use this in the reply when an item is popped
  • Using a message inspector to manipulate the messages

I've no idea if any of these will work, what would be involved or what is the best solution. Do you?

Bennet answered 9/4, 2013 at 12:51 Comment(2)
Will you be changing objects on a service, or you just need to store them and send back to clients?Kirchner
Just storing and sending them back. I need to know about the base type as that has some properties the service uses to influence the storage, but it's read-only as far as the service is concerned.Bennet
B
3

I have made some headway with this using a message inspector and a placeholder type that implements IExtensibleDataObject. The inspector maniuplates the incoming message and changes the type hint to that of the placeholder and adds the original type as a property. When the type is then sent out in a reply the opposite happens, thereby making the placeholder looks like the original type.

My grievance with this solution is that it is bound to the service because I have had to include the service's XML namespace and explicitly name the methods and parameters to be maniuplated. Other than that is seems to work fairly well, though I have only tested it on fairly simple types derived from BaseType.

Can anyone improve on this? There's a bounty in it for you.

public class PlaceholderType : BaseType, IExtensibleDataObject
{
    [IgnoreDataMember]
    public string OriginalTypeName { get; set; }

    [IgnoreDataMember]
    public string OriginalNamespace { get; set; }

    ExtensionDataObject IExtensibleDataObject.ExtensionData { get; set; }
}

public class FunkadelicInspector : IDispatchMessageInspector, IContractBehavior
{
    const string PlaceholderNamespace = "http://my.placeholder.namespace";
    const string ServiceNamespace = "http://tempuri.org/";

    public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
    {
        XmlDocument xmlDoc = ReadMessage(request);

        XmlNamespaceManager nsmgr = new XmlNamespaceManager(xmlDoc.NameTable);
        // Dislike: having to know the service namespace, method and parameters
        nsmgr.AddNamespace("s", ServiceNamespace);
        XmlNode itemElement = xmlDoc.SelectSingleNode("//s:Push/s:item", nsmgr);

        if (itemElement != null)
        {
            XmlAttribute typeAttribute = itemElement.Attributes["type", "http://www.w3.org/2001/XMLSchema-instance"];
            if (typeAttribute != null)
            {
                // Record original type
                string[] parts = typeAttribute.Value.Split(':');
                string originalTypeName = parts[1];
                // Replace with placeholder type
                typeAttribute.Value = parts[0] + ":" + typeof(PlaceholderType).FullName;

                // Record original assembly
                XmlAttribute nsAtt = itemElement.Attributes["xmlns:" + parts[0]];
                string originalAssembly = nsAtt.Value;
                // Replace with placeholder type's assembly
                nsAtt.Value = typeof(PlaceholderType).Assembly.FullName;

                // Add placeholders
                itemElement.AppendChild(xmlDoc.CreateElement("OriginalType", PlaceholderNamespace)).InnerText = originalTypeName;
                itemElement.AppendChild(xmlDoc.CreateElement("OriginalAssembly", PlaceholderNamespace)).InnerText = originalAssembly;
            }
        }

        //Now recreate the message
        request = WriteMessage(request, xmlDoc);
        return null;
    }

    public void BeforeSendReply(ref Message reply, object correlationState)
    {
        XmlDocument xmlDoc = ReadMessage(reply);

        XmlNamespaceManager nsmgr = new XmlNamespaceManager(xmlDoc.NameTable);
        nsmgr.AddNamespace("s", ServiceNamespace);
        nsmgr.AddNamespace("plc", PlaceholderNamespace);
        // Dislike: having to know the service namespace, method and parameters
        XmlNode resultElement = xmlDoc.SelectSingleNode("//s:PopResponse/s:PopResult", nsmgr);

        if (resultElement != null)
        {
            XmlElement originalType = resultElement.SelectSingleNode("plc:OriginalType", nsmgr) as XmlElement;
            XmlElement originalAssembly = resultElement.SelectSingleNode("plc:OriginalAssembly", nsmgr) as XmlElement;
            if (originalType != null && originalAssembly != null)
            {
                // Replace original type
                XmlAttribute type = resultElement.Attributes["type", "http://www.w3.org/2001/XMLSchema-instance"];
                string[] parts = type.Value.Split(':'); // 0 is an alias for the assembly, 1 is the type
                type.Value = parts[0] + ":" + originalType.InnerText;

                // Replace original assembly
                XmlAttribute ass = resultElement.Attributes["xmlns:" + parts[0]];
                ass.Value = originalAssembly.InnerText;

                // Remove placeholders
                resultElement.RemoveChild(originalType);
                resultElement.RemoveChild(originalAssembly);
            }
        }

        //Now recreate the message
        reply = WriteMessage(reply, xmlDoc);
    }

    private static Message WriteMessage(Message original, XmlDocument xmlDoc)
    {
        MemoryStream ms = new MemoryStream();
        xmlDoc.Save(ms);
        ms.Position = 0;
        XmlReader reader = XmlReader.Create(ms);
        Message newMessage = Message.CreateMessage(reader, int.MaxValue, original.Version);
        newMessage.Properties.CopyProperties(original.Properties);
        return newMessage;
    }

    private static XmlDocument ReadMessage(Message message)
    {
        MemoryStream ms = new MemoryStream();
        using (XmlWriter writer = XmlWriter.Create(ms))
        {
            message.WriteMessage(writer); // the message was consumed here
            writer.Flush();
        }
        ms.Position = 0;
        XmlDocument xmlDoc = new XmlDocument();
        xmlDoc.Load(ms);
        return xmlDoc;
    }

    void IContractBehavior.AddBindingParameters(ContractDescription contractDescription, ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
    {
    }

    void IContractBehavior.ApplyClientBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, ClientRuntime clientRuntime)
    {
    }

    void IContractBehavior.ApplyDispatchBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, DispatchRuntime dispatchRuntime)
    {
        dispatchRuntime.MessageInspectors.Add(this);
    }

    void IContractBehavior.Validate(ContractDescription contractDescription, ServiceEndpoint endpoint)
    {
    }
}
Bennet answered 16/4, 2013 at 8:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.