I have a C# WCF service using a webHttpBinding endpoint that will receive and return data in JSON format. The data to send/receive needs to use a polymorphic type so that data of different types can be exchanged in the same “data packet”. I have the following data model:
[DataContract]
public class DataPacket
{
[DataMember]
public List<DataEvent> DataEvents { get; set; }
}
[DataContract]
[KnownType(typeof(IntEvent))]
[KnownType(typeof(BoolEvent))]
public class DataEvent
{
[DataMember]
public ulong Id { get; set; }
[DataMember]
public DateTime Timestamp { get; set; }
public override string ToString()
{
return string.Format("DataEvent: {0}, {1}", Id, Timestamp);
}
}
[DataContract]
public class IntEvent : DataEvent
{
[DataMember]
public int Value { get; set; }
public override string ToString()
{
return string.Format("IntEvent: {0}, {1}, {2}", Id, Timestamp, Value);
}
}
[DataContract]
public class BoolEvent : DataEvent
{
[DataMember]
public bool Value { get; set; }
public override string ToString()
{
return string.Format("BoolEvent: {0}, {1}, {2}", Id, Timestamp, Value);
}
}
My service will send/receive the sub-type events (IntEvent, BoolEvent etc.) in a single data packet, as follows:
[ServiceContract]
public interface IDataService
{
[OperationContract]
[WebGet(UriTemplate = "GetExampleDataEvents")]
DataPacket GetExampleDataEvents();
[OperationContract]
[WebInvoke(UriTemplate = "SubmitDataEvents", RequestFormat = WebMessageFormat.Json)]
void SubmitDataEvents(DataPacket dataPacket);
}
public class DataService : IDataService
{
public DataPacket GetExampleDataEvents()
{
return new DataPacket {
DataEvents = new List<DataEvent>
{
new IntEvent { Id = 12345, Timestamp = DateTime.Now, Value = 5 },
new BoolEvent { Id = 45678, Timestamp = DateTime.Now, Value = true }
}
};
}
public void SubmitDataEvents(DataPacket dataPacket)
{
int i = dataPacket.DataEvents.Count; //dataPacket contains 2 events, but both are type DataEvent instead of IntEvent and BoolEvent
IntEvent intEvent = dataPacket.DataEvents[0] as IntEvent;
Console.WriteLine(intEvent.Value); //null pointer as intEvent is null since the cast failed
}
}
When I submit my packet to the SubmitDataEvents method though, I get DataEvent types and trying to cast them back to their base types (just for testing purposes) results in an InvalidCastException. My packet is:
POST http://localhost:4965/DataService.svc/SubmitDataEvents HTTP/1.1
User-Agent: Fiddler
Host: localhost:4965
Content-Type: text/json
Content-Length: 340
{
"DataEvents": [{
"__type": "IntEvent:#WcfTest.Data",
"Id": 12345,
"Timestamp": "\/Date(1324905383689+0000)\/",
"Value": 5
}, {
"__type": "BoolEvent:#WcfTest.Data",
"Id": 45678,
"Timestamp": "\/Date(1324905383689+0000)\/",
"Value": true
}]
}
Apologies for the long post, but is there anything I can do to preserve the base types of each object? I thought adding the type hint to the JSON and the KnownType attributes to DataEvent would allow me to preserve the types – but it doesn’t seem to work.
Edit: If I send the request to SubmitDataEvents in XML format (with Content-Type: text/xml instead of text/json) then the List<DataEvent> DataEvents does contain the sub-types instead of the super-type. As soon as I set the request to text/json and send the above packet then I only get the super-type and I can’t cast them to the sub-type. My XML request body is:
<ArrayOfDataEvent xmlns="http://schemas.datacontract.org/2004/07/WcfTest.Data">
<DataEvent i:type="IntEvent" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<Id>12345</Id>
<Timestamp>1999-05-31T11:20:00</Timestamp>
<Value>5</Value>
</DataEvent>
<DataEvent i:type="BoolEvent" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<Id>56789</Id>
<Timestamp>1999-05-31T11:20:00</Timestamp>
<Value>true</Value>
</DataEvent>
</ArrayOfDataEvent>
Edit 2: Updated service description after Pavel’s comments below. This still doesn’t work when sending the JSON packet in Fiddler2. I just get a List containing DataEvent instead of IntEvent and BoolEvent.
Edit 3: As Pavel suggested, here is the output from System.ServiceModel.OperationContext.Current.RequestContext.RequestMessage.ToString(). Looks OK to me.
<root type="object">
<DataEvents type="array">
<item type="object">
<__type type="string">IntEvent:#WcfTest.Data</__type>
<Id type="number">12345</Id>
<Timestamp type="string">/Date(1324905383689+0000)/</Timestamp>
<Value type="number">5</Value>
</item>
<item type="object">
<__type type="string">BoolEvent:#WcfTest.Data</__type>
<Id type="number">45678</Id>
<Timestamp type="string">/Date(1324905383689+0000)/</Timestamp>
<Value type="boolean">true</Value>
</item>
</DataEvents>
</root>
When tracing the deserialization of the packet, I get the following messages in the trace:
<TraceRecord xmlns="http://schemas.microsoft.com/2004/10/E2ETraceEvent/TraceRecord" Severity="Verbose">
<TraceIdentifier>http://msdn.microsoft.com/en-GB/library/System.Runtime.Serialization.ElementIgnored.aspx</TraceIdentifier>
<Description>An unrecognized element was encountered in the XML during deserialization which was ignored.</Description>
<AppDomain>1c7ccc3b-4-129695001952729398</AppDomain>
<ExtendedData xmlns="http://schemas.microsoft.com/2006/08/ServiceModel/StringTraceRecord">
<Element>:__type</Element>
</ExtendedData>
</TraceRecord>
This message is repeated 4 times (twice with __type as the element and twice with Value). Looks like the type hinting information is being ignored then the Value elements are ignored as the packet is deserialized to DataEvent instead of IntEvent/BoolEvent.
Thanks to Pavel Gatilov, I’ve now found the solution to this problem. I’ll add it as separate answer here for anyone who may be caught out by this in future.
The problem is that the JSON deserializer doesn’t seem to be very accepting of whitespace. The data in the packet that I was sending was “pretty printed” with line breaks and spaces to make it more readable. However, when this packet was deserialized, this meant that when looking for the
"__type"hint, the JSON deserializer was looking at the wrong part of the packet. This meant that the type hint was missed and the packet was deserialized as the wrong type.The following packet works correctly:
However, this packet doesn’t work:
These packets are exactly the same apart from the line breaks and spaces.