【问题标题】:Preserving Polymorphic Types in a WCF Service using JSON使用 JSON 在 WCF 服务中保留多态类型
【发布时间】:2011-12-24 16:59:14
【问题描述】:

我有一个使用 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&lt;DataEvent&gt; 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 的 cmets 之后的服务描述。在 Fiddler2 中发送 JSON 数据包时,这仍然不起作用。我只是得到一个包含DataEvent 而不是IntEventBoolEventList

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

【问题讨论】:

    标签: c# wcf json polymorphism


    【解决方案1】:

    在处理序列化时,尝试先序列化一个对象图,看看序列化后的字符串格式。然后使用该格式生成正确的序列化字符串。

    您的数据包不正确。正确的是:

    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

    多么奇怪……它在我的机器上运行。

    我在 Win7 x64 上使用带有最新更新的 .NET 4 和 VS2010。

    我接受您的服务合同、实施和数据合同。我将它们托管在 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);
      }
    }
    

    请注意,调试器将项目详细信息显示为DataEvents,但字符串表示形式和详细信息中的第一项清楚地表明所有子类型都已很好地反序列化:

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

    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 字段名称错误的消息发出任何警告,但它可能会针对您的问题原因向您显示一些提示。

    我的请求消息

    似乎这就是问题所在:虽然您将__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
        } 
      } 
    }
    

    我试图找到该属性用于确定反序列化类型的位置,但没有成功。

    希望这会有所帮助。

    【讨论】:

    • 我发送的数据包是通过首先调用GetExampleDataEvents()生成的,然后将JSON中的名称更改为dataEvents。我没有包含 HTTP 标头,但我像您一样使用 Fiddler2(包括 Content-Type: text/json.
    • 这很奇怪,因为我发布的JSON正是GetExampleDataEvents()返回的JSON没有任何修改。你试过了吗?它奏效了吗?如果是这样,你为什么要改变它?当我尝试您的 JSON 时,我得到了空列表,因为它需要另一个包含该列表的类。
    • 我已根据您的上述建议更新了问题描述。恐怕还是没有运气!
    • @AdamRodger 我已经更新了我的答案,以清楚地展示我为测试您的代码所做的工作。我还添加了一些有关进一步故障排除的提示。事实上,我的想法已经不多了。
    • 我已经从这个页面复制并粘贴了所有代码到一个新的解决方案中(修复命名空间等),但它仍然不起作用。在您的调试器屏幕截图中,它显示了IntEventBoolEvent,我的两个都显示为DataEvent。我会尝试按照您的建议进行 WCF 跟踪。同时,您有什么方法可以将您的解决方案发送给我,以便我尝试找出差异?
    【解决方案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
        }]
    }
    

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

    【讨论】:

    • 哇...很高兴知道您的问题已得到解决。但是,格式化的 JSON 对我来说效果很好,我总是将它复制粘贴到 Fiddler 并且没有问题。我确实相信您没有安装 .NET 框架的所有更新,或者最新版本存在与空白字符处理相关的奇怪错误。我真的很尴尬。
    • 别不好意思,如果没有你的帮助,我永远不会发现这一点。我发现的唯一方法是调试您在上面发布的 WCF 源代码。我注意到offset 的值总是太低,并且碰巧注意到消息缓冲区中包含\r\n 字符会以某种方式抛出偏移计算。我可能会发布另一个问题,看看是否有人知道为什么空格会干扰解析,因为我希望它的灵活性被包含或不包含。
    • 哦,那应该是“困惑”而不是“尴尬”:)。对了,我找到了类似问题的描述in this article
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-04-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多