【问题标题】:WCF client, XML namespace prefix results in null objectWCF 客户端,XML 命名空间前缀导致空对象
【发布时间】:2019-12-20 18:44:58
【问题描述】:

我创建了 WCF 服务 (.NET 4.6.2),第三方创建了客户端。

我无法控制客户端,也无法让第三方更改任何内容,因此任何更改都将仅在服务器端进行。

我已将代码最小化并匿名化到最低限度,希望能以尽可能少的代码证明问题。如果我犯了任何错误或您需要任何进一步的细节,我会相应地更改细节。


总结

客户端能够调用服务,解析器能够从 SOAP Action 中正确识别代码中的方法,但是传入的任何对象始终为空。我能够通过断点捕获请求并查看对象在运行时为空。

我已经确定问题主要是由客户端传递的 XML 命名空间前缀引起的。

我能够拦截传入的原始消息,并且可以准确地看到客户端发送的内容。

我能够操作这些传入的消息进行实验,然后将修改后的消息版本发布到服务,以使用基于通用浏览器的客户端测试结果。


示例数据合同:-

[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 方发布到服务的内容:-

/*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 仍然为空

/*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 仍然为空

/*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 中所理解的那样,前缀实际上与确定对象无关,因此 &lt;ns1:Something&gt; 应该与 &lt;ns2:Something&gt;&lt;Something&gt; 相同的对象 然而,WCF 解析器确定 &lt;ns1:Something&gt; 不是 &lt;Something&gt;


如何在服务器端解决这个问题,最好是从 WCF 服务中解决?我在想是否有一种方法可以在 WCF 服务中解析消息之前捕获消息并预先去除 ns1: 和空白 xmlns?

非常感谢

编辑

这是一个示例,您可以复制/粘贴以准确重现问题:-

创建 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 服务并将此消息发布到该服务,您将收到“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>

这是我无法解决的第一个问题

【问题讨论】:

    标签: c# xml wcf


    【解决方案1】:

    尝试以下:

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

    【讨论】:

    • 我已经尝试将 Namespace = "" 装饰添加到 SomeObject 类,但这与请求中存在 时发生的故障没有区别。此外,我没有看到任何解决命名空间前缀问题的方法
    • 代码与您发布的 xml 一起使用这是第 3 方发布到服务的内容:
    • 我同意,您的示例作为一个独立的应用程序工作,但是,根据我原来的问题,由于命名空间前缀,我无法让它在我的 WCF 服务中工作,这个问题没有已解决。
    • WCF 服务得到了什么?如果示例正常工作,那么您的提供商可能有无效的 xml。我需要查看实际的 xml(或修改以删除机密信息)来提供帮助。
    • 我将我的问题中的示例创建为 WCF 服务。如果我详细传递原始请求,则“SomeObject”对象为空。如果我详细修改请求(以删除 ns1 前缀和空白 xmlns),那么 SomeObject 是有效的,并且 SomeField 按预期填充了“Blah”。这个概念似乎完全一样。在一个简单的 1 个对象示例中缩小问题范围比包含大约 100 个对象的原始示例要容易得多。如果有任何进一步的信息会有所帮助,请告诉我
    【解决方案2】:

    我已投入大量时间寻找一种简单的解决方案,但似乎并不存在。

    为了解决这个问题,我最终研究了 MessageInspectors,它允许在 WCF 服务处理原始 SOAP 消息之前查看和编辑它们。

    创建一个实现 IDispatchMessageInspector 的类。这是实际过滤发生的地方

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

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

    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>
    

    这是一个相当重量级的解决方案,但确实可以完美运行,学习此功能肯定会对未来的项目有用。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-09-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多