Сохранение полиморфных типов в службе WCF с использованием JSON
У меня есть служба WCF С# с использованием конечной точки webHttpBinding, которая будет получать и возвращать данные в формате JSON. Данные для отправки/получения должны использовать полиморфный тип, чтобы данные разных типов могли быть обменены в одном и том же "пакете данных". У меня есть следующая модель данных:
[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);
}
}
Моя служба будет отправлять/получать события подтипа (IntEvent, BoolEvent и т.д.) в одном пакете данных следующим образом:
[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
}
}
Когда я отправляю свой пакет методу SubmitDataEvents
, однако, я получаю типы DataEvent
и пытаюсь вернуть их к базовым типам (только для целей тестирования), приводит к InvalidCastException
. Мой пакет:
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
}]
}
Извините за длинный пост, но есть ли что-нибудь, что я могу сделать, чтобы сохранить базовые типы каждого объекта? Я подумал, добавив подсказку типа к JSON, а атрибуты KnownType в DataEvent
позволят мне сохранять типы, но он, похоже, не работает.
Изменить. Если я отправлю запрос в SubmitDataEvents
в формате XML (с Content-Type: text/xml
вместо text/json
), то List<DataEvent> DataEvents
содержит подтипы, а не супер -тип. Как только я установлю запрос на text/json
и отправлю вышеуказанный пакет, я получаю только супертип, и я не могу отнести их к подтипу. Тело моего XML-запроса:
<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>
Изменить 2. Обновленное описание сервиса после комментариев Павла ниже. Это по-прежнему не работает при отправке пакета JSON в Fiddler2. Я просто получаю List
, содержащий DataEvent
вместо IntEvent
и BoolEvent
.
Изменить 3. Как предложил Павел, вот результат от System.ServiceModel.OperationContext.Current.RequestContext.RequestMessage.ToString()
. Похоже на меня.
<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>
При трассировке десериализации пакета я получаю следующие сообщения в трассировке:
<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>
Это сообщение повторяется 4 раза (дважды с __type
как элемент и дважды с Value
). Похоже, что информация о намеченном типе игнорируется, а элементы Value
игнорируются, поскольку пакет десериализуется до DataEvent
вместо IntEvent
/BoolEvent
.
Ответы
Ответ 1
Благодаря Павлу Гатилову, я нашел решение этой проблемы. Я добавлю это как отдельный ответ здесь для тех, кто может быть пойман этим в будущем.
Проблема заключается в том, что десериализатор JSON, похоже, не очень-то воспринимает пробелы. Данные в пакете, который я отправлял, были "довольно напечатаны" с разрывами строк и пробелами, чтобы сделать его более читаемым. Однако, когда этот пакет был десериализован, это означало, что при поиске подсказки "__type"
десериализатор JSON просматривал неправильную часть пакета. Это означало, что подсказка типа была пропущена, и пакет был десериализован как неправильный тип.
Следующий пакет работает правильно:
POST http://localhost:6463/DataService.svc/SubmitDataEvents HTTP/1.1
User-Agent: Fiddler
Content-Type: text/json
Host: localhost:6463
Content-Length: 233
{"DataEvents":[{"__type":"IntEvent:#WebApplication1","Id":12345,"Timestamp":"\/Date(1324905383689+0000)\/","IntValue":5},{"__type":"BoolEvent:#WebApplication1","Id":45678,"Timestamp":"\/Date(1324905383689+0000)\/","BoolValue":true}]}
Однако этот пакет не работает:
POST http://localhost:6463/DataService.svc/SubmitDataEvents HTTP/1.1
User-Agent: Fiddler
Content-Type: text/json
Host: localhost:6463
Content-Length: 343
{
"DataEvents": [{
"__type": "IntEvent:#WebApplication1",
"Id": 12345,
"Timestamp": "\/Date(1324905383689+0000)\/",
"IntValue": 5
}, {
"__type": "BoolEvent:#WebApplication1",
"Id": 45678,
"Timestamp": "\/Date(1324905383689+0000)\/",
"BoolValue": true
}]
}
Эти пакеты точно такие же, кроме разрывов строк и пробелов.
Ответ 2
Всякий раз, когда вы имеете дело с сериализацией, попробуйте сначала сериализовать граф объектов, чтобы увидеть последовательный формат строки. Затем используйте формат для создания правильных сериализованных строк.
Ваш пакет неверен. Правильный:
POST http://localhost:47440/Service1.svc/SubmitDataEvents HTTP/1.1
User-Agent: Fiddler
Host: localhost:47440
Content-Length: 211
Content-Type: text/json
[
{
"__type":"IntEvent:#WcfTest.Data",
"Id":12345,
"Timestamp":"\/Date(1324757832735+0700)\/",
"Value":5
},
{
"__type":"BoolEvent:#WcfTest.Data",
"Id":45678,
"Timestamp":"\/Date(1324757832736+0700)\/",
"Value":true
}
]
Обратите внимание на заголовок Content-Type
.
Я пробовал это с вашим кодом, и он отлично работает (ну, я удалил Console.WriteLine
и протестировал в отладчике). Вся иерархия классов прекрасна, все объекты могут быть добавлены к их типам. Он работает.
UPDATE
JSON, который вы опубликовали, работает со следующим кодом:
[DataContract]
public class SomeClass
{
[DataMember]
public List<DataEvent> dataEvents { get; set; }
}
...
[ServiceContract]
public interface IDataService
{
...
[OperationContract]
[WebInvoke(UriTemplate = "SubmitDataEvents")]
void SubmitDataEvents(SomeClass parameter);
}
Обратите внимание, что к дереву объектов добавляется еще один высокий уровень node.
И снова он отлично работает с наследованием.
Если проблема по-прежнему остается, отправьте код, который вы используете для вызова службы, а также сведения об исключении, которые вы получаете.
ОБНОВЛЕНИЕ 2
Как странно... Он работает на моей машине.
Я использую .NET 4 и VS2010 с последними обновлениями на Win7 x64.
Я принимаю ваши контракты на обслуживание, реализацию и передачу данных. Я размещаю их в веб-приложении под Cassini. У меня есть следующий web.config:
<configuration>
<connectionStrings>
<!-- excluded for brevity -->
</connectionStrings>
<system.web>
<!-- excluded for brevity -->
</system.web>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>
<system.serviceModel>
<behaviors>
<serviceBehaviors>
<behavior name="">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="false" />
</behavior>
</serviceBehaviors>
<endpointBehaviors>
<behavior name="WebBehavior">
<webHttp />
</behavior>
</endpointBehaviors>
</behaviors>
<serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
<services>
<service name="WebApplication1.DataService">
<endpoint address="ws" binding="wsHttpBinding" contract="WebApplication1.IDataService"/>
<endpoint address="" behaviorConfiguration="WebBehavior"
binding="webHttpBinding"
contract="WebApplication1.IDataService">
</endpoint>
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
</service>
</services>
</system.serviceModel>
</configuration>
Теперь я делаю следующий POST от Fiddler2 (важно: я переименовал пространство имен производных типов в соответствие с моим случаем):
POST http://localhost:47440/Service1.svc/SubmitDataEvents HTTP/1.1
User-Agent: Fiddler
Content-Type: text/json
Host: localhost:47440
Content-Length: 336
{
"DataEvents": [{
"__type": "IntEvent:#WebApplication1",
"Id": 12345,
"Timestamp": "\/Date(1324905383689+0000)\/",
"Value": 5
}, {
"__type": "BoolEvent:#WebApplication1",
"Id": 45678,
"Timestamp": "\/Date(1324905383689+0000)\/",
"Value": true
}]
}
Тогда у меня есть следующий код в реализации службы:
public void SubmitDataEvents(DataPacket parameter)
{
foreach (DataEvent dataEvent in parameter.DataEvents)
{
var message = dataEvent.ToString();
Debug.WriteLine(message);
}
}
Обратите внимание, что отладчик показывает данные деталей как DataEvent
s, но представления строк и первый элемент в деталях ясно показывают, что все подтипы были десериализованы хорошо:
![Debugger screenshot]()
И вывод отладки содержит следующее после того, как я ударил метод:
IntEvent: 12345, 26.12.2011 20:16:23, 5
BoolEvent: 45678, 26.12.2011 20:16:23, True
Я также пытался запустить его под IIS (на Win7), и все работает отлично.
У меня был только базовый тип десериализован после того, как я испортил пакет, удалив один знак подчеркивания из имени поля __type
. Если я изменю значение __type
, вызов будет сбой во время десериализации, он не попадет в службу.
Вот что вы могли бы попробовать:
- Убедитесь, что у вас нет отладочных сообщений, исключений и т.д. (проверьте вывод Debug).
- Создайте новое решение для чистого веб-приложения, вставьте требуемый код и проверьте, работает ли он там. Если это так, то ваш исходный проект должен иметь некоторые странные настройки конфигурации.
- В отладчике проанализируйте
System.ServiceModel.OperationContext.Current.RequestContext.RequestMessage.ToString()
в окне просмотра. Он будет содержать XML-сообщение, переведенное с вашего JSON. Проверьте правильность.
- Проверьте, есть ли ожидающие обновления для .NET.
- Попробуйте отслеживать WCF. Хотя это, похоже, не вызывает никаких предупреждений о сообщениях с неправильным именем поля
__type
, может случиться так, что он покажет вам несколько советов по причинам, связанным с вашими проблемами.
My RequestMessage
Похоже, что вот трек проблемы: в то время как у вас есть __type
как элемент, у меня есть его как атрибут. Предположительно, ваши сборки WCF имеют ошибку в преобразовании JSON в XML
<root type="object">
<DataEvents type="array">
<item type="object" __type="IntEvent:#WebApplication1">
<Id type="number">12345</Id>
<Timestamp type="string">/Date(1324905383689+0000)/</Timestamp>
<Value type="number">5</Value>
</item>
<item type="object" __type="BoolEvent:#WebApplication1">
<Id type="number">45678</Id>
<Timestamp type="string">/Date(1324905383689+0000)/</Timestamp>
<Value type="boolean">true</Value>
</item>
</DataEvents>
</root>
Я нашел место, где обрабатывается __type
. Вот он:
// from System.Runtime.Serialization.Json.XmlJsonReader, System.Runtime.Serialization, Version=4.0.0.0
void ReadServerTypeAttribute(bool consumedObjectChar)
{
int offset;
int offsetMax;
int correction = consumedObjectChar ? -1 : 0;
byte[] buffer = BufferReader.GetBuffer(9 + correction, out offset, out offsetMax);
if (offset + 9 + correction <= offsetMax)
{
if (buffer[offset + correction + 1] == (byte) '\"' &&
buffer[offset + correction + 2] == (byte) '_' &&
buffer[offset + correction + 3] == (byte) '_' &&
buffer[offset + correction + 4] == (byte) 't' &&
buffer[offset + correction + 5] == (byte) 'y' &&
buffer[offset + correction + 6] == (byte) 'p' &&
buffer[offset + correction + 7] == (byte) 'e' &&
buffer[offset + correction + 8] == (byte) '\"')
{
// It attribute!
XmlAttributeNode attribute = AddAttribute();
// the rest is omitted for brevity
}
}
}
Я попытался найти место, где атрибут используется для определения десериализованного типа, но не повезло.
Надеюсь, что это поможет.