WCF 客户端,XML 命名空间前缀导致空对象
WCF client, XML namespace prefix results in null object
我已经创建了一个 WCF 服务 (.NET 4.6.2),并且第三方已经创建了客户端。
我无法控制客户端,也无法让第三方更改任何内容,因此任何更改都将仅在服务器端进行。
我已将代码最小化并匿名化到最低限度,希望能用尽可能少的代码来演示问题。如果我犯了任何错误或者您需要任何进一步的详细信息,我将相应地更改详细信息。
总结
客户端能够调用服务,解析器能够正确识别来自 SOAP Action 的代码中的方法,但是传入的任何对象始终为 null。我能够使用断点捕获请求,并看到对象在运行时为空。
我发现问题主要是由客户端传递的 XML 名称空间前缀引起的。
我能够拦截传入的原始消息,并且我可以准确地看到客户端发送的内容。
我能够操纵这些传入消息进行实验,然后 post 将消息的修改版本发送到服务,以使用基于通用浏览器的客户端测试结果。
示例数据协定:-
[DataContract(Namespace = "http://mycompanyname.co.uk")]
public class SomeObject
{
[DataMember]
public string SomeField { get; set; }
}
示例服务合同:-
[ServiceContract(Namespace = "http://mycompanyname.co.uk")]
public interface IIncoming
{
[OperationContract]
XmlElement MyAction(SomeObject someObject);
}
实施先前定义的服务契约的示例 WCF 服务:-
[ServiceBehavior(Namespace = "http://mycompanyname.co.uk")]
public class Incoming : IIncoming
{
public XmlElement MyAction(SomeObject someObject)
{
XmlDocument response = new XmlDocument();
response.LoadXml("<Response>OK</Response>");
return response.DocumentElement;
}
}
这是第 3 方 post 向服务发送的内容:-
/*3rd party original*/
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body>
<ns1:MyAction xmlns:ns1="http://mycompanyname.co.uk">
<someObject xmlns="">
<SomeField>Blah</SomeField>
</someObject>
</ns1:MyAction>
</soapenv:Body>
</soapenv:Envelope>
断点代码,我可以看到上面的结果是调用了 MyAction,但是 MyObject 是 null
我修改了 SOAP 消息以删除空白的 xmlns,但 someObject 仍然为 null
/*blank xmlns removed*/
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body>
<ns1:MyAction xmlns:ns1="http://mycompanyname.co.uk">
<someObject>
<SomeField>Blah</SomeField>
</someObject>
</ns1:MyAction>
</soapenv:Body>
</soapenv:Envelope>
我修改了 SOAP 消息以删除 ns1 前缀,但 someObject 仍然为 null
/*ns1 removed*/
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body>
<MyAction xmlns="http://mycompanyname.co.uk">
<someObject xmlns="">
<SomeField>Blah</SomeField>
</someObject>
</MyAction>
</soapenv:Body>
</soapenv:Envelope>
现在,如果我同时删除 ns1 前缀和空白 xmlns,则 someObject 会按预期正确填充。这解决了一切。唯一的问题是,我无法让客户进行此更改。
/*both removed - works*/
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body>
<MyAction xmlns="http://mycompanyname.co.uk">
<someObject>
<SomeField>Blah</SomeField>
</someObject>
</MyAction>
</soapenv:Body>
</soapenv:Envelope>
首先,为什么会这样?正如我在 XML 中所理解的那样,前缀实际上与确定对象无关,因此 <ns1:Something>
应该与 <ns2:Something>
或 <Something>
是同一个对象
然而,WCF 解析器确定 <ns1:Something>
不是 <Something>
如何在服务器端解决这个问题,最好是从 WCF 服务内部解决?我在想是否有一种方法可以在 WCF 服务中解析消息之前捕获消息并预先去除 ns1: 和空白 xmlns?
非常感谢
编辑
这是一个示例,您可以 copy/paste 准确重现问题:-
创建 WCF 服务应用程序 (.NET Framework 4.6.2)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.Xml;
namespace GenericIncoming
{
[ServiceContract(Namespace = "http://mycompanyname.co.uk")]
public interface IIncoming
{
[OperationContract]
XmlElement MyAction(SomeObject someObject);
}
[DataContract(Namespace ="")]
public class SomeObject
{
[DataMember]
public string SomeField { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.Xml;
namespace GenericIncoming
{
[ServiceBehavior(Namespace = "http://mycompanyname.co.uk")]
public class Incoming : IIncoming
{
public XmlElement MyAction(SomeObject someObject)
{
XmlDocument response = new XmlDocument();
if (someObject != null)
{
response.LoadXml("<Response>OK</Response>");
}
else
{
response.LoadXml("<Response>NULL</Response>");
}
return response.DocumentElement;
}
}
}
运行 您的 WCF 服务和 POST 此消息到该服务,您将收到 "OK" 响应
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<MyAction xmlns="http://mycompanyname.co.uk">
<someObject>
<SomeField>Blah</SomeField>
</someObject>
</MyAction>
</s:Body>
</s:Envelope>
现在如果你添加 ns1 前缀,就像第 3 方发送给我的那样,那么响应是 "NULL" 这意味着 WCF 中的对象是空的,因为解析器无法处理XML
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<ns1:MyAction xmlns:ns1="http://mycompanyname.co.uk">
<someObject>
<SomeField>Blah</SomeField>
</someObject>
</ns1:MyAction>
</s:Body>
</s:Envelope>
这是我无法解决的第一个问题
尝试以下操作:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
using System.IO;
namespace ConsoleApplication1
{
public class Program
{
const string FILENAME = @"c:\temp\test.xml";
static void Main(string[] args)
{
string response = File.ReadAllText(FILENAME);
StringReader sReader = new StringReader(response);
XmlSerializer serializer = new XmlSerializer(typeof(Envelope));
Envelope envelope = (Envelope)serializer.Deserialize(sReader);
}
}
[XmlRoot(ElementName = "Envelope", Namespace = "http://schemas.xmlsoap.org/soap/envelope/")]
public class Envelope
{
[XmlElement(ElementName = "Body", Namespace = "http://schemas.xmlsoap.org/soap/envelope/")]
public Body body { get;set;}
}
[XmlRoot(ElementName = "Body", Namespace = "http://schemas.xmlsoap.org/soap/envelope/")]
public class Body
{
[XmlElement(ElementName = "MyAction", Namespace = "http://mycompanyname.co.uk")]
public MyAction myAction { get; set; }
}
[XmlRoot(ElementName = "MyAction", Namespace = "http://mycompanyname.co.uk")]
public class MyAction
{
[XmlElement(ElementName = "someObject", Namespace = "")]
public SomeObject someObject { get; set; }
}
[XmlRoot(ElementName = "someObject", Namespace = "")]
public class SomeObject
{
public string SomeField { get; set; }
}
}
我投入了大量时间寻找简单的解决方案,但似乎不存在。
为了解决这个问题,我最终研究了 MessageInspectors,它允许在原始 SOAP 消息被 WCF 服务处理之前查看和编辑它们。
创建一个实现 IDispatchMessageInspector 的 class。这是实际过滤发生的地方
public class CorrectorInspector : IDispatchMessageInspector
{
public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
{
request = FilterMessage(request);
return null;
}
public void BeforeSendReply(ref Message reply, object correlationState)
{
return;
}
private Message FilterMessage(Message originalMessage)
{
MemoryStream memoryStream = new MemoryStream();
XmlWriter xmlWriter = XmlWriter.Create(memoryStream);
originalMessage.WriteMessage(xmlWriter);
xmlWriter.Flush();
string body = Encoding.UTF8.GetString(memoryStream.ToArray());
xmlWriter.Close();
//Remove the ns1 prefix
body = body.Replace("ns1:", "");
body = body.Replace(":ns1", "");
//remove the blank namespace
body = body.Replace(" xmlns=\"\"","");
memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(body));
XmlDictionaryReader xmlDictionaryReader = XmlDictionaryReader.CreateTextReader(memoryStream, new XmlDictionaryReaderQuotas());
Message newMessage = Message.CreateMessage(xmlDictionaryReader, int.MaxValue, originalMessage.Version);
newMessage.Properties.CopyProperties(originalMessage.Properties);
return newMessage;
}
}
创建一个实现 IEndpointBehavior
的 class
public class CorrectorBehavior : IEndpointBehavior
{
public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
{
return;
}
public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
return;
}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
{
CorrectorInspector inspector = new CorrectorInspector();
endpointDispatcher.DispatchRuntime.MessageInspectors.Add(inspector);
}
public void Validate(ServiceEndpoint endpoint)
{
return;
}
}
创建一个实现 BehaviorExtensionElement
的 class
public class CorrectorBehaviourExtensionElement : BehaviorExtensionElement
{
public override Type BehaviorType
{
get
{
return typeof(CorrectorBehavior);
}
}
protected override object CreateBehavior()
{
return new CorrectorBehavior();
}
}
在web.config中,我们需要定义behaviorExtension,serviceBehavior,然后我们需要针对服务绑定指定我们新创建的行为
<system.serviceModel>
<services>
<service name="GenericIncoming.Incoming">
<endpoint address="" binding="basicHttpBinding" contract="GenericIncoming.IIncoming" behaviorConfiguration="correctorBehavior" />
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior>
<serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/>
<serviceDebug includeExceptionDetailInFaults="false"/>
</behavior>
</serviceBehaviors>
<endpointBehaviors>
<behavior name="correctorBehavior">
<serviceFilter />
</behavior>
</endpointBehaviors>
</behaviors>
<extensions>
<behaviorExtensions>
<add name="serviceFilter" type="GenericIncoming.CorrectorBehaviourExtensionElement, GenericIncoming, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</behaviorExtensions>
</extensions>
<protocolMapping>
<add binding="basicHttpsBinding" scheme="https" />
</protocolMapping>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />
</system.serviceModel>
这是一个相当重量级的解决方案,但它确实完美地工作,学习这个功能肯定对以后的项目有用。
我已经创建了一个 WCF 服务 (.NET 4.6.2),并且第三方已经创建了客户端。
我无法控制客户端,也无法让第三方更改任何内容,因此任何更改都将仅在服务器端进行。
我已将代码最小化并匿名化到最低限度,希望能用尽可能少的代码来演示问题。如果我犯了任何错误或者您需要任何进一步的详细信息,我将相应地更改详细信息。
总结
客户端能够调用服务,解析器能够正确识别来自 SOAP Action 的代码中的方法,但是传入的任何对象始终为 null。我能够使用断点捕获请求,并看到对象在运行时为空。
我发现问题主要是由客户端传递的 XML 名称空间前缀引起的。
我能够拦截传入的原始消息,并且我可以准确地看到客户端发送的内容。
我能够操纵这些传入消息进行实验,然后 post 将消息的修改版本发送到服务,以使用基于通用浏览器的客户端测试结果。
示例数据协定:-
[DataContract(Namespace = "http://mycompanyname.co.uk")]
public class SomeObject
{
[DataMember]
public string SomeField { get; set; }
}
示例服务合同:-
[ServiceContract(Namespace = "http://mycompanyname.co.uk")]
public interface IIncoming
{
[OperationContract]
XmlElement MyAction(SomeObject someObject);
}
实施先前定义的服务契约的示例 WCF 服务:-
[ServiceBehavior(Namespace = "http://mycompanyname.co.uk")]
public class Incoming : IIncoming
{
public XmlElement MyAction(SomeObject someObject)
{
XmlDocument response = new XmlDocument();
response.LoadXml("<Response>OK</Response>");
return response.DocumentElement;
}
}
这是第 3 方 post 向服务发送的内容:-
/*3rd party original*/
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body>
<ns1:MyAction xmlns:ns1="http://mycompanyname.co.uk">
<someObject xmlns="">
<SomeField>Blah</SomeField>
</someObject>
</ns1:MyAction>
</soapenv:Body>
</soapenv:Envelope>
断点代码,我可以看到上面的结果是调用了 MyAction,但是 MyObject 是 null
我修改了 SOAP 消息以删除空白的 xmlns,但 someObject 仍然为 null
/*blank xmlns removed*/
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body>
<ns1:MyAction xmlns:ns1="http://mycompanyname.co.uk">
<someObject>
<SomeField>Blah</SomeField>
</someObject>
</ns1:MyAction>
</soapenv:Body>
</soapenv:Envelope>
我修改了 SOAP 消息以删除 ns1 前缀,但 someObject 仍然为 null
/*ns1 removed*/
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body>
<MyAction xmlns="http://mycompanyname.co.uk">
<someObject xmlns="">
<SomeField>Blah</SomeField>
</someObject>
</MyAction>
</soapenv:Body>
</soapenv:Envelope>
现在,如果我同时删除 ns1 前缀和空白 xmlns,则 someObject 会按预期正确填充。这解决了一切。唯一的问题是,我无法让客户进行此更改。
/*both removed - works*/
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body>
<MyAction xmlns="http://mycompanyname.co.uk">
<someObject>
<SomeField>Blah</SomeField>
</someObject>
</MyAction>
</soapenv:Body>
</soapenv:Envelope>
首先,为什么会这样?正如我在 XML 中所理解的那样,前缀实际上与确定对象无关,因此 <ns1:Something>
应该与 <ns2:Something>
或 <Something>
是同一个对象
然而,WCF 解析器确定 <ns1:Something>
不是 <Something>
如何在服务器端解决这个问题,最好是从 WCF 服务内部解决?我在想是否有一种方法可以在 WCF 服务中解析消息之前捕获消息并预先去除 ns1: 和空白 xmlns?
非常感谢
编辑
这是一个示例,您可以 copy/paste 准确重现问题:-
创建 WCF 服务应用程序 (.NET Framework 4.6.2)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.Xml;
namespace GenericIncoming
{
[ServiceContract(Namespace = "http://mycompanyname.co.uk")]
public interface IIncoming
{
[OperationContract]
XmlElement MyAction(SomeObject someObject);
}
[DataContract(Namespace ="")]
public class SomeObject
{
[DataMember]
public string SomeField { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.Xml;
namespace GenericIncoming
{
[ServiceBehavior(Namespace = "http://mycompanyname.co.uk")]
public class Incoming : IIncoming
{
public XmlElement MyAction(SomeObject someObject)
{
XmlDocument response = new XmlDocument();
if (someObject != null)
{
response.LoadXml("<Response>OK</Response>");
}
else
{
response.LoadXml("<Response>NULL</Response>");
}
return response.DocumentElement;
}
}
}
运行 您的 WCF 服务和 POST 此消息到该服务,您将收到 "OK" 响应
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<MyAction xmlns="http://mycompanyname.co.uk">
<someObject>
<SomeField>Blah</SomeField>
</someObject>
</MyAction>
</s:Body>
</s:Envelope>
现在如果你添加 ns1 前缀,就像第 3 方发送给我的那样,那么响应是 "NULL" 这意味着 WCF 中的对象是空的,因为解析器无法处理XML
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<ns1:MyAction xmlns:ns1="http://mycompanyname.co.uk">
<someObject>
<SomeField>Blah</SomeField>
</someObject>
</ns1:MyAction>
</s:Body>
</s:Envelope>
这是我无法解决的第一个问题
尝试以下操作:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
using System.IO;
namespace ConsoleApplication1
{
public class Program
{
const string FILENAME = @"c:\temp\test.xml";
static void Main(string[] args)
{
string response = File.ReadAllText(FILENAME);
StringReader sReader = new StringReader(response);
XmlSerializer serializer = new XmlSerializer(typeof(Envelope));
Envelope envelope = (Envelope)serializer.Deserialize(sReader);
}
}
[XmlRoot(ElementName = "Envelope", Namespace = "http://schemas.xmlsoap.org/soap/envelope/")]
public class Envelope
{
[XmlElement(ElementName = "Body", Namespace = "http://schemas.xmlsoap.org/soap/envelope/")]
public Body body { get;set;}
}
[XmlRoot(ElementName = "Body", Namespace = "http://schemas.xmlsoap.org/soap/envelope/")]
public class Body
{
[XmlElement(ElementName = "MyAction", Namespace = "http://mycompanyname.co.uk")]
public MyAction myAction { get; set; }
}
[XmlRoot(ElementName = "MyAction", Namespace = "http://mycompanyname.co.uk")]
public class MyAction
{
[XmlElement(ElementName = "someObject", Namespace = "")]
public SomeObject someObject { get; set; }
}
[XmlRoot(ElementName = "someObject", Namespace = "")]
public class SomeObject
{
public string SomeField { get; set; }
}
}
我投入了大量时间寻找简单的解决方案,但似乎不存在。
为了解决这个问题,我最终研究了 MessageInspectors,它允许在原始 SOAP 消息被 WCF 服务处理之前查看和编辑它们。
创建一个实现 IDispatchMessageInspector 的 class。这是实际过滤发生的地方
public class CorrectorInspector : IDispatchMessageInspector
{
public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
{
request = FilterMessage(request);
return null;
}
public void BeforeSendReply(ref Message reply, object correlationState)
{
return;
}
private Message FilterMessage(Message originalMessage)
{
MemoryStream memoryStream = new MemoryStream();
XmlWriter xmlWriter = XmlWriter.Create(memoryStream);
originalMessage.WriteMessage(xmlWriter);
xmlWriter.Flush();
string body = Encoding.UTF8.GetString(memoryStream.ToArray());
xmlWriter.Close();
//Remove the ns1 prefix
body = body.Replace("ns1:", "");
body = body.Replace(":ns1", "");
//remove the blank namespace
body = body.Replace(" xmlns=\"\"","");
memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(body));
XmlDictionaryReader xmlDictionaryReader = XmlDictionaryReader.CreateTextReader(memoryStream, new XmlDictionaryReaderQuotas());
Message newMessage = Message.CreateMessage(xmlDictionaryReader, int.MaxValue, originalMessage.Version);
newMessage.Properties.CopyProperties(originalMessage.Properties);
return newMessage;
}
}
创建一个实现 IEndpointBehavior
的 classpublic class CorrectorBehavior : IEndpointBehavior
{
public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
{
return;
}
public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
return;
}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
{
CorrectorInspector inspector = new CorrectorInspector();
endpointDispatcher.DispatchRuntime.MessageInspectors.Add(inspector);
}
public void Validate(ServiceEndpoint endpoint)
{
return;
}
}
创建一个实现 BehaviorExtensionElement
的 classpublic class CorrectorBehaviourExtensionElement : BehaviorExtensionElement
{
public override Type BehaviorType
{
get
{
return typeof(CorrectorBehavior);
}
}
protected override object CreateBehavior()
{
return new CorrectorBehavior();
}
}
在web.config中,我们需要定义behaviorExtension,serviceBehavior,然后我们需要针对服务绑定指定我们新创建的行为
<system.serviceModel>
<services>
<service name="GenericIncoming.Incoming">
<endpoint address="" binding="basicHttpBinding" contract="GenericIncoming.IIncoming" behaviorConfiguration="correctorBehavior" />
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior>
<serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/>
<serviceDebug includeExceptionDetailInFaults="false"/>
</behavior>
</serviceBehaviors>
<endpointBehaviors>
<behavior name="correctorBehavior">
<serviceFilter />
</behavior>
</endpointBehaviors>
</behaviors>
<extensions>
<behaviorExtensions>
<add name="serviceFilter" type="GenericIncoming.CorrectorBehaviourExtensionElement, GenericIncoming, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</behaviorExtensions>
</extensions>
<protocolMapping>
<add binding="basicHttpsBinding" scheme="https" />
</protocolMapping>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />
</system.serviceModel>
这是一个相当重量级的解决方案,但它确实完美地工作,学习这个功能肯定对以后的项目有用。