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:
DataContractResolver
which might involve a TypeDelegator
IDataContractSurrogate
in place of an unshared typeI've no idea if any of these will work, what would be involved or what is the best solution. Do you?
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)
{
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With