2011-12-24 15 views
11

Mam usługę C# WCF, używając punktu końcowego webHttpBinding, który będzie odbierać i zwracać dane w formacie JSON. Dane wysyłane/odbierane muszą być typu polimorficznego, aby dane różnych typów mogły być wymieniane w tym samym "pakiecie danych". Mam następujący model danych:Zachowanie polimorficznych typów w usłudze WCF przy użyciu 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); 
    } 
} 

Moja usługa będzie wysyłać/odbierać zdarzenia podtyp (IntEvent, BoolEvent etc.) w jednym pakiecie danych, jak następuje:

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

Kiedy Prześlij mój pakiet do metody SubmitDataEvents, jednak otrzymuję typy DataEvent i próbuję odesłać je z powrotem do ich typów bazowych (tylko w celach testowych) w wyniku InvalidCastException. Moja paczka jest:

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

Przepraszam za długi post, ale jest coś, co można zrobić, aby zachować typy bazowe każdego obiektu? Pomyślałem, że dodanie wskazówki typu do JSON i atrybutów KnownType do DataEvent pozwoliłoby mi zachować typy - ale wydaje się, że nie działa.

Edit: Jeśli mogę wysłać wniosek do SubmitDataEvents w formacie XML (z Content-Type: text/xml zamiast text/json) wtedy List<DataEvent> DataEvents nie zawierają podtypy zamiast super-type. Jak tylko ustawię żądanie na text/json i wyślę powyższy pakiet, otrzymam tylko supertyp i nie mogę ich przesłać do podtypu. Moja prośba ciało XML jest:

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

Edycja 2: Poprawiono opis usługi po komentarzach Pavel jest poniżej. To nadal nie działa podczas wysyłania pakietu JSON w Fiddler2. Właśnie dostaję List zawierający DataEvent zamiast IntEvent i BoolEvent.

Edytuj 3: Jak sugerował Pavel, tutaj jest wyjście z System.ServiceModel.OperationContext.Current.RequestContext.RequestMessage.ToString(). Wygląda mi dobrze.

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

Podczas śledzenia deserializacji opakowania, pojawiają się następujące komunikaty w śladowych:

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

Komunikat ten powtarza się 4-krotnie (dwukrotnie __type jako element i dwukrotnie Value). Wygląda na to, że informacje o podpowiedzi typu są ignorowane, a elementy Value są ignorowane, ponieważ pakiet jest przekształcany do postaci szeregowej na DataEvent zamiast IntEvent/BoolEvent.

Odpowiedz

2

Dzięki Pavelowi Gatilovowi znalazłem rozwiązanie tego problemu. Dodam go tutaj jako osobną odpowiedź dla każdego, kto może być w przyszłości przyłapany na tym.

Problem polega na tym, że deserializator JSON wydaje się nie akceptować białych znaków. Dane w pakiecie, który wysyłałem, były "ładnie wydrukowane" z podziałem linii i spacji, aby uczynić go bardziej czytelnym. Jednakże, gdy ten pakiet został zserializowany, oznaczało to, że podczas wyszukiwania deserializatora JSON patrzył na niewłaściwą część pakietu. Oznaczało to, że podpowiedź typu została pominięta, a pakiet został zserializowany jako niewłaściwy typ.

Poniższy pakiet działa poprawnie:

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

Jednak ten pakiet nie działa:

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

Te pakiety są dokładnie takie same, niezależnie od podziałów wierszy i przestrzeni.

+0

Wow ... Cieszę się, że Twój problem został rozwiązany.Jednak sformatowany JSON działał dobrze dla mnie, zawsze kopiowałem-wklejałem do Fiddlera i nie miałem problemów. Sądzę, że albo nie zainstalowałeś wszystkich aktualizacji dla platformy .NET, albo najnowsza wersja ma dziwny błąd związany z przetwarzaniem znaków białych znaków. Jestem całkowicie zakłopotany. –

+0

Nie wstydź się, nigdy bym tego nie znalazł bez twojej pomocy. Jedyny sposób, jaki odkryłem, polegał na debugowaniu kodu źródłowego WCF, który napisałeś powyżej. Zauważyłem, że wartość 'offset' była zawsze zbyt niska i zauważyłem, że włączenie znaków' \ r' i '\ n' w buforze komunikatów w jakiś sposób wyrzuciło obliczenia przesunięcia. Mogę zadać kolejne pytanie, aby sprawdzić, czy ktoś wie, dlaczego białe spacje zakłócają parsowanie, ponieważ chciałbym, aby elastyczność została uwzględniona lub nie. –

+1

Och, to powinno być raczej "zdezorientowane" niż "zawstydzone" :). Przy okazji, znalazłem opis podobnego problemu [w tym artykule] (http://blog.js-development.com/2010/05/n-will-break-your-json-jquery-wcf.html). –

3

Zawsze, gdy mamy do czynienia z serializacją, spróbuj najpierw serializować wykresy obiektów, aby zobaczyć format serializowanych ciągów. Następnie użyj formatu, aby utworzyć poprawne serializowane ciągi.

Twój pakiet jest niepoprawny. Prawidłowy to:

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

Uwaga nagłówek Content-Type również.

Próbowałem go z Twoim kodem i działa idealnie (cóż, usunąłem Console.WriteLine i przetestowałem w debugerze). Cała hierarchia klas jest w porządku, wszystkie obiekty można rzutować na ich typy. To działa.

UPDATE

JSON pan pisał prace z następującego kodu:

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

... 

[ServiceContract] 
public interface IDataService 
{ 
    ... 

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

Zauważ, że inny węzeł wysoki poziom zostanie dodany do drzewa obiektów.

I znowu działa dobrze z dziedziczeniem.

Jeśli problem nadal występuje, opublikuj kod, którego używasz do wywołania usługi, a także szczegóły dotyczące wyjątków.

UPDATE 2

Jak dziwne ... To działa na moim komputerze.

Używam .NET 4 i VS2010 z najnowszymi aktualizacjami na Win7 x64.

Przyjmuję umowę o świadczenie usług, umowę o realizację i dane. Hostuję je w aplikacji internetowej pod Cassini. Mam następujący 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> 

Teraz robię następujący post przez Fiddler2 (ważne: Mam przemianowany nazw typów pochodnych dopasować moim przypadku):

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

Następnie mam następujący kod w celu wykonania usług:

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

Zauważ, że debugger pokazuje szczegóły przedmiotów, jak DataEvent s, ale reprezentacje smyczkowych i pierwsza pozycja w szczegółach s jasno pokazują, że wszystkie podtypy zostały rozszeregować dobrze: Debugger screenshot

i debugowanie wyjściowy zawiera następujące po tym jak uderzył w metodę:

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

Próbowałem zostały również uruchomienie go pod IIS (na Win7) i wszystko działa dobrze.

Po deseniu usunięto tylko podstawowy typ bazy, usuwając jeden podkreślenie z nazwy pola __type. Jeśli zmienię wartość __type, połączenie zawiesi się podczas deserializacji, nie trafi w usługę.

Oto co można spróbować:

  1. Upewnij się, że nie mają żadnych wiadomości debugowania, wyjątki itp (sprawdź Debug Output).
  2. Utwórz nowe, czyste rozwiązanie aplikacji WWW, wklej wymagany kod i sprawdź, czy działa. Jeśli tak, to oryginalny projekt musi mieć dziwne ustawienia konfiguracyjne.
  3. W debugerze przeanalizuj System.ServiceModel.OperationContext.Current.RequestContext.RequestMessage.ToString() w oknie Watch.Będzie zawierał komunikat XML przetłumaczony z twojego JSON. Sprawdź, czy jest poprawna.
  4. Sprawdź, czy masz jakieś oczekujące aktualizacje dla .NET.
  5. Wypróbuj tracing WCF. Chociaż wydaje się, że nie wysyła żadnych ostrzeżeń dla wiadomości o nieprawidłowej nazwie pola __type, może się zdarzyć, że pokaże kilka wskazówek dotyczących przyczyn problemów.

Moja RequestMessage

Wygląda tu jest utwór o numerze: gdy masz __type jako elementu, muszę go jako atrybut. Podobno wasze zespoły WCF mają błąd w tłumaczeniu JSON do 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> 

Znalazłem miejsce, gdzie __type jest przetwarzany. Oto ona:

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

Starałem się znaleźć miejsce, w którym atrybut jest używany do określenia rozszeregować typu, ale bez powodzenia.

Mam nadzieję, że to pomoże.

+0

Pakiet, który wysyłałem został wygenerowany przez wywołanie najpierw 'GetExampleDataEvents()', a następnie zmianę nazwy w JSON na 'dataEvents'. Nie uwzględniłem nagłówków HTTP, ale zrobiłem to, co zrobiłeś i wysłałeś w pakiecie używając Fiddler2 (w tym 'Content-Type: text/json'. –

+0

To bardzo dziwne, ponieważ opublikowany przeze mnie JSON ** jest dokładnie ** JSON zwrócony przez 'GetExampleDataEvents()' ** bez żadnych modyfikacji **. Czy próbowałeś? Czy to działało? Jeśli tak, to dlaczego próbujesz to zmienić? Kiedy wypróbowałem twój JSON, dostałem pustą listę , ponieważ wymaga innej klasy, która zawierałaby listę –

+0

Zaktualizowałem opis problemu z powyższymi zaleceniami. Wciąż nie ma szczęścia, obawiam się! –

Powiązane problemy