Сохранение полиморфных типов в службе 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
    } 
  } 
}

Я попытался найти место, где атрибут используется для определения десериализованного типа, но не повезло.

Надеюсь, что это поможет.