2011-12-24 26 views
11

Tengo un servicio C# WCF utilizando un punto final webHttpBinding que recibirá y devolverá datos en formato JSON. Los datos a enviar/recibir necesitan usar un tipo polimórfico para que los datos de diferentes tipos puedan intercambiarse en el mismo "paquete de datos". Tengo el siguiente modelo de datos:Conservación de tipos polimórficos en un servicio WCF utilizando 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); 
    } 
} 

Mi servicio será enviar/recibir los eventos sub-tipo (IntEvent, BoolEvent etc.) en un solo paquete de datos, como sigue:

[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 
    } 
} 

Cuando sin embargo, enviar mi paquete al método SubmitDataEvents, obtengo los tipos DataEvent y trato de devolverlos a sus tipos base (solo para fines de prueba) da como resultado un InvalidCastException. Mi paquete es:

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 
    }] 
} 

Disculpas por el largo post, pero ¿hay algo que pueda hacer para preservar los tipos de base de cada objeto? Pensé que agregar la sugerencia de tipo a los atributos JSON y KnownType a DataEvent me permitiría conservar los tipos, pero parece que no funciona.

Editar: Si envío de la solicitud a SubmitDataEvents en formato XML (con Content-Type: text/xml en lugar de text/json) entonces el List<DataEvent> DataEvents contiene los subtipos lugar del supertipo. Tan pronto como establezco la solicitud en text/json y envío el paquete anterior, solo obtengo el super-tipo y no puedo convertirlo al subtipo. Mi solicitud XML cuerpo es:

<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> 

Editar 2: descripción del servicio Actualización después de los comentarios de Pavel abajo. Esto aún no funciona al enviar el paquete JSON en Fiddler2. Acabo de obtener un List que contiene DataEvent en lugar de IntEvent y BoolEvent.

Editar 3: Como sugirió Pavel, aquí está la salida de System.ServiceModel.OperationContext.Current.RequestContext.RequestMessage.ToString(). Me parece bien

<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> 

Cuando trazando la deserialización del paquete, consigo los siguientes mensajes en la traza:

<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> 

Este mensaje se repite 4 veces (dos veces con __type como el elemento y dos veces con Value). Parece que se está ignorando la información de sugerencia tipo, entonces los elementos Value se ignoran ya que el paquete se deserializa a DataEvent en lugar de IntEvent/BoolEvent.

Respuesta

2

Gracias a Pavel Gatilov, he encontrado la solución a este problema. Lo agregaré como una respuesta separada aquí para cualquier persona que pueda ser atrapada por esto en el futuro.

El problema es que el deserializador JSON no parece ser muy compatible con los espacios en blanco. Los datos en el paquete que estaba enviando estaban "bastante impresos" con saltos de línea y espacios para hacerlo más legible. Sin embargo, cuando este paquete se deserializó, esto significaba que al buscar la sugerencia "__type", el deserializador JSON estaba buscando la parte incorrecta del paquete. Esto significaba que la sugerencia de tipo se había omitido y el paquete se había deserializado como el tipo incorrecto.

El paquete siguiente funciona correctamente:

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}]} 

Sin embargo, este paquete no funciona:

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 
    }] 
} 

Estos paquetes son exactamente los mismos, aparte de los saltos de línea y espacios.

+0

Wow ... Me alegra saber que su problema ha sido resuelto.Sin embargo, el JSON formateado funcionó bien para mí, siempre copié y pegué al Fiddler y no tuve problemas. Creo que no ha instalado todas las actualizaciones para .NET framework, o que la última versión tiene un extraño error relacionado con el procesamiento de caracteres en espacio en blanco. Estoy completamente avergonzado. –

+0

No se avergüence del todo, nunca lo hubiera encontrado sin su ayuda. La única forma en que me enteré fue mediante la depuración del código fuente de WCF que publicó anteriormente. Noté que el valor de 'offset' era siempre demasiado bajo y me di cuenta de que la inclusión de los caracteres' \ r' y '\ n' en el búfer de mensajes estaba arrojando el cálculo de compensación de alguna manera. Puedo publicar otra pregunta para ver si alguien sabe por qué el espacio en blanco interfiere con el análisis sintáctico, ya que me gustaría tener la flexibilidad de incluirlo o no. –

+1

Oh, eso debería haber sido 'confundido' en lugar de 'avergonzado' :). Por cierto, he encontrado la descripción de un problema similar [en este artículo] (http://blog.js-development.com/2010/05/n-will-break-your-json-jquery-wcf.html) –

3

Cuando se trata de serialización, intente serializar primero un gráfico de objetos para ver el formato de cadena serializada. Luego use el formato para producir cadenas serializadas correctas.

Su paquete es incorrecto. La correcta es:

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 
    } 
] 

Nota la cabecera Content-Type también.

Lo he intentado con su código y funciona perfectamente (bueno, he eliminado el Console.WriteLine y probado en el depurador). Toda la jerarquía de clases está bien, todos los objetos se pueden convertir a sus tipos. Funciona.

ACTUALIZACIÓN

El JSON has publicado trabajos con el siguiente código:

[DataContract] 
public class SomeClass 
{ 
    [DataMember] 
    public List<DataEvent> dataEvents { get; set; } 
} 

... 

[ServiceContract] 
public interface IDataService 
{ 
    ... 

    [OperationContract] 
    [WebInvoke(UriTemplate = "SubmitDataEvents")] 
    void SubmitDataEvents(SomeClass parameter); 
} 

Tenga en cuenta que otro nodo de alto nivel se añade al árbol de objetos.

Y de nuevo, funciona bien con la herencia.

Si el problema persiste, publique el código que utiliza para invocar el servicio, así como los detalles de las excepciones que obtenga.

ACTUALIZACIÓN 2

Qué extraño ... funciona en mi máquina.

Uso .NET 4 y VS2010 con las últimas actualizaciones de Win7 x64.

Tomo su contrato de servicio, implementación y contratos de datos. Los alojé en una aplicación web bajo Cassini. Tengo el siguiente 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> 

ahora hago la siguiente POSTAL por Fiddler2 (importante: He cambiado el nombre del espacio de nombres de los tipos derivados de igualar mi caso):

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 
    }] 
} 

Entonces tengo el siguiente código en la implementación del servicio:

public void SubmitDataEvents(DataPacket parameter) 
{ 
    foreach (DataEvent dataEvent in parameter.DataEvents) 
    { 
    var message = dataEvent.ToString(); 
    Debug.WriteLine(message); 
    } 
} 

tenga en cuenta que el depurador muestra los detalles como artículos DataEvent s, pero representaciones de cadena y el primer elemento en el detalle s muestran claramente que todos los subtipos han sido deserializado así: Debugger screenshot

y depurar salida contiene lo siguiente después me golpeó el método:

IntEvent: 12345, 26.12.2011 20:16:23, 5 
BoolEvent: 45678, 26.12.2011 20:16:23, True 

También he intentado ejecutarlo bajo el IIS (en Win7) y todo funciona bien también.

He deserializado el tipo base después de haber dañado el paquete eliminando un guión bajo del nombre de campo __type. Si modifico el valor de __type, la llamada se bloqueará durante la deserialización, no afectará el servicio.

Aquí es lo que usted podría intentar:

  1. Asegúrese de que usted no tiene ningún mensajes de depuración, excepciones, etc (compruebe la salida de depuración).
  2. Crea una nueva solución de aplicación web limpia, pega el código requerido y prueba si funciona allí. Si lo hace, entonces su proyecto original debe tener algunas configuraciones de configuración raras.
  3. En el depurador, analice System.ServiceModel.OperationContext.Current.RequestContext.RequestMessage.ToString() en la ventana Inspección.Contendrá el mensaje XML traducido de su JSON. Verifica si es correcto.
  4. Compruebe si tiene actualizaciones pendientes para .NET.
  5. Probar tracing WCF. Aunque no parece emitir ninguna advertencia para los mensajes con el nombre de campo __type incorrecto, es posible que le muestre algunos consejos por los motivos de su problema.

Mi RequestMessage

parece que aquí es la vía de la cuestión: mientras tiene __type como elemento, que lo tienen como atributo. Supuestamente, los ensamblados WCF tienen un error en JSON a la traducción 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> 

que he encontrado el lugar donde se procesa __type. Aquí está:

// 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's attribute! 
     XmlAttributeNode attribute = AddAttribute(); 
     // the rest is omitted for brevity 
    } 
    } 
} 

He tratado de encontrar el lugar donde se utiliza el atributo para determinar el tipo deserializado, pero sin suerte.

Espero que esto ayude.

+0

El paquete que estaba enviando se generó al llamar 'GetExampleDataEvents()' primero, y luego cambiar el nombre en el JSON a 'dataEvents'. No incluí los encabezados HTTP pero hice lo que hice y envié el paquete usando Fiddler2 (incluido el 'Content-Type: text/json'. –

+0

Eso es muy extraño, porque el JSON que publiqué es ** exactamente ** el JSON devuelto por 'GetExampleDataEvents()' ** sin modificaciones **. ¿Lo has probado? ¿Funcionó? Si lo hizo, ¿por qué estás tratando de cambiarlo? Cuando probé tu JSON, obtuve la lista vacía , porque requiere otra clase que contendría la lista. –

+0

He actualizado la descripción del problema con sus recomendaciones anteriores. Todavía no tengo suerte, me temo! –

Cuestiones relacionadas