使用JSON在WCF服务中保留多态类型

时间:2011-12-24 16:59:14

标签: c# wcf json polymorphism

我有一个使用webHttpBinding端点的C#WCF服务,该端点将以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将允许我保留类型 - 但它似乎不起作用。

修改:如果我以XML格式(SubmitDataEvents代替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 :在下面的Pavel评论之后更新了服务说明。在Fiddler2中发送JSON数据包时,这仍然不起作用。我只收到List DataEvent而不是IntEventBoolEvent

编辑3 :正如Pavel建议的那样,这是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

2 个答案:

答案 0 :(得分:3)

每当处理序列化时,首先尝试序列化对象图以查看序列化字符串格式。然后使用该格式生成正确的序列化字符串。

您的信息包不正确。正确的是:

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并在调试器中进行了测试)。所有类层次结构都很好,所有对象都可以转换为它们的类型。它有效。

<强>更新

您发布的JSON使用以下代码:

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

...

[ServiceContract]
public interface IDataService
{
  ...

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

请注意,另一个高级节点将添加到对象树中。

同样,它继承可以正常工作。

如果问题仍然存在,请发布您用于调用服务的代码,以及您获得的异常详细信息。

更新2

有多奇怪......它适用于我的机器。

我使用.NET 4和VS2010以及Win7 x64上的最新更新。

我接受您的服务合同,实施和数据合同。我将它们托管在Cassini下的Web应用程序中。我有以下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>

现在我通过Fiddler2进行以下POST(重要的是:我已经重命名派生类型的命名空间以匹配我的情况):

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,但字符串表示形式和详细信息中的第一项清楚地表明所有子类型都已反序列化: Debugger screenshot

在我点击方法后,调试输出包含以下内容:

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

我也尝试在IIS下运行它(在Win7上),一切正常。

我通过删除__type字段名称中的一个下划线来损坏数据包后,我只有反序列化的基类型。如果我修改__type的值,则在反序列化期间调用将崩溃,它将不会命中该服务。

以下是您可以尝试的内容:

  1. 确保您没有任何调试消息,异常等(请检查调试输出)。
  2. 创建一个新的干净的Web应用程序解决方案,粘贴所需的代码并测试它是否在那里工作。如果是,那么原始项目必须有一些奇怪的配置设置。
  3. 在调试器中,分析Watch窗口中的System.ServiceModel.OperationContext.Current.RequestContext.RequestMessage.ToString()。它将包含从您的JSON转换的XML消息。检查一下是否正确。
  4. 检查您是否有.NET的待处理更新。
  5. 试试tracing WCF。虽然它似乎没有为错误的__type字段名称的消息发出任何警告,但可能会出现一些提示,说明您的问题原因。
  6. 我的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's attribute!
          XmlAttributeNode attribute = AddAttribute(); 
          // the rest is omitted for brevity
        } 
      } 
    }
    

    我试图找到使用该属性来确定反序列化类型的地方,但没有运气。

    希望这有帮助。

答案 1 :(得分:2)

感谢Pavel Gatilov,我现在找到了解决这个问题的方法。对于今后可能会被这个人抓住的人,我会在此处单独添加答案。

问题是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
    }]
}

除了换行符和空格之外,这些数据包完全相同。