在WCF

时间:2016-06-30 20:50:15

标签: xml wcf xaml soap

我正在尝试将几个XAML文档片段作为basicHttpBinding实例传递给SOAP XElement Web服务。

[ServiceContract(Namespace = "someNamespace")]
interface IMyService
{
    [OperationContract]
    void Start(XElement someArgument);
}

消息的结尾格式如下。

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:exec="someNamespace"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:system="clr-namespace:System;assembly=mscorlib">
   <soapenv:Header/>
   <soapenv:Body>
      <exec:Start>
         <exec:someArgument>
            <SomeObject xmlns="someAnotherNamespace"
                        xmlns:myPrefix="yetAnotherNamespace">
               <AnotherObject Property="{myPrefix:Foo}" />
            </SomeObject>
         </exec:someArgument>
      </exec:Start>
   </soapenv:Body>
</soapenv:Envelope>

一切都很好myPrefix是在XAML岛的根上定义的(即<SomeObject>)。但是,如果我将名称空间声明移动到<soapenv:Envelope>(显然是允许的,禁止任何前缀冲突)

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:exec="someNamespace"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:system="clr-namespace:System;assembly=mscorlib"
    xmlns:myPrefix="yetAnotherNamespace">
   <soapenv:Header/>
   <soapenv:Body>
      <exec:Start>
         <exec:someArgument>
            <SomeObject xmlns="someAnotherNamespace">
               <AnotherObject Property="{myPrefix:Foo}" />
            </SomeObject>
         </exec:someArgument>
      </exec:Start>
   </soapenv:Body>
</soapenv:Envelope>

结果XElement不再了解URI yetAnotherNamespace实际上由前缀myPrefix表示。而是分配了通用前缀(p2)。

因此,XamlXmlReader将无法读取标记扩展名{myPrefix:Foo},因为它仍与旧前缀myPrefix而非p2相关联。实际上,对于WCF内部的XML处理,标记扩展是一个字符串属性,并没有收到名称空间前缀的特定处理。

成功读取XML岛的行为取决于xmlns:前缀声明的位置非常容易混淆。 有没有办法以某种方式重新映射前缀以获得更正确的行为?

1 个答案:

答案 0 :(得分:0)

玩了一下我意识到如果我使用XmlElement而不是XElement,问题就会消失,因为原始前缀都会被保留。但是,我仍然很好奇是否存在更优雅的解决方案...

不幸的是,从System.Xml.Linq还原到System.Xml没有帮助,因为如果命名空间前缀仅用于标记扩展(而不是标记),则其前缀将无法恢复。相反,我实现了一个IDispatchMessageInspector,它重新声明SOAP信封中所选XML元素的名称空间(从整个信封继承xmlns:声明),以便使XML的选定片段自包含。

using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Configuration;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;

namespace RedeclareNamespaces
{
    internal class RedeclareNamespacesMessageInspector : IDispatchMessageInspector, IEndpointBehavior
    {
        private readonly IReadOnlyCollection<string> actionFilters;
        private readonly IXmlNamespaceResolver namespaceResolver;
        private readonly string targetElements;

        public RedeclareNamespacesMessageInspector(IReadOnlyCollection<string> actionFilters,
            IXmlNamespaceResolver namespaceResolver, string targetElements)
        {
            this.actionFilters = actionFilters;
            this.namespaceResolver = namespaceResolver;
            this.targetElements = targetElements;
        }

        public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
        {
            if (ShouldProcessRequest(request))
                DoAfterReceiveRequest(ref request);
            return null;
        }

        private bool ShouldProcessRequest(Message request)
        {
            return actionFilters == null || actionFilters.Contains(request.Headers.Action);
        }

        private void DoAfterReceiveRequest(ref Message request)
        {
            var document = ReadXDocument(request);
            RedeclareNamespaces(document);
            var xmlReader = CreateMessageXmlReader(document);
            request = Message.CreateMessage(xmlReader, int.MaxValue, request.Headers.MessageVersion);
        }

        public void BeforeSendReply(ref Message reply, object correlationState)
        {
            // Nothing to fix here.
        }

        private static XDocument ReadXDocument(Message request)
        {
            // A buffered copy of the message must be created before the whole envelope can be read.
            var buffer = request.CreateBufferedCopy(int.MaxValue);
            var copyOfRequest = buffer.CreateMessage();

            var document = new XDocument();
            using (var writer = document.CreateWriter())
            {
                copyOfRequest.WriteMessage(writer);
                writer.Flush();
            }
            return document;
        }

        private void RedeclareNamespaces(XDocument document)
        {
            var elementsToFix = document.XPathSelectElements(targetElements, namespaceResolver);
            foreach (var element in elementsToFix)
            {
                var inheritedXmlnsAttributes = GetInheritedXmlnsAttributes(element);
                element.Add(inheritedXmlnsAttributes);
            }
        }

        private object[] GetInheritedXmlnsAttributes(XElement targetElement)
        {
            var prefixesSeen = new HashSet<string>();
            var attributes = new List<object>();
            var element = targetElement;
            while (element != null)
            {
                for (var attribute = element.FirstAttribute; attribute != null; attribute = attribute.NextAttribute)
                {
                    string localName = attribute.Name.LocalName;
                    if (!attribute.IsNamespaceDeclaration || prefixesSeen.Contains(localName))
                        continue;
                    prefixesSeen.Add(localName);
                    // Do not add attributes declared on the element itself twice, but mark them as seen.
                    if (element != targetElement)
                        attributes.Add(new XAttribute(attribute));
                }
                element = element.Parent;
            }
            return attributes.ToArray();
        }

        private static XmlReader CreateMessageXmlReader(XDocument document)
        {
            // Do not dispose this XmlReader and Stream before the message is consumed.
            var stream = new MemoryStream();
            // We must save the message into a buffer, directly reading the XDocument nodes is not supported by WCF.
            document.Save(stream, SaveOptions.DisableFormatting);
            stream.Position = 0;
            var xmlReader = XmlReader.Create(stream);
            return xmlReader;
        }

        void IEndpointBehavior.Validate(ServiceEndpoint endpoint)
        {
            // Nothing to do.
        }

        void IEndpointBehavior.AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
        {
            // Nothing to do.
        }

        void IEndpointBehavior.ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
        {
            endpointDispatcher.DispatchRuntime.MessageInspectors.Add(this);
        }

        void IEndpointBehavior.ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
        {
            // Nothing to do.
        }
    }

    public class RedeclareNamespacesConfigurationSection : BehaviorExtensionElement
    {
        private const string ActionFilterAttribute = "actionFilter";

        [ConfigurationProperty(ActionFilterAttribute)]
        public string ActionFilter
        {
            get { return (string) this[ActionFilterAttribute]; }
            set { this[ActionFilterAttribute] = value; }
        }

        private const string NamespacesAttribute = "namespaces";

        [ConfigurationProperty(NamespacesAttribute)]
        public string Namespaces
        {
            get { return (string)this[NamespacesAttribute]; }
            set { this[NamespacesAttribute] = value; }
        }

        private const string TargetElementsAttribute = "targetElements";

        [ConfigurationProperty(TargetElementsAttribute)]
        public string TargetElements
        {
            get { return (string)this[TargetElementsAttribute]; }
            set { this[TargetElementsAttribute] = value; }
        }

        protected override object CreateBehavior()
        {
            var actionFilters = ActionFilter?.Split(';').Select(i => i.Trim()).ToArray();
            var namespaceResolver = CreateNamespaceResolver();
            if (TargetElements == null)
                throw new ArgumentNullException(TargetElementsAttribute, "TargetElements query must be provided.");
            return new RedeclareNamespacesMessageInspector(actionFilters, namespaceResolver, TargetElements);
        }

        public override Type BehaviorType => typeof(RedeclareNamespacesMessageInspector);

        private XmlNamespaceManager CreateNamespaceResolver()
        {
            var namespaceResolver = new XmlNamespaceManager(new NameTable());
            if (Namespaces != null)
            {
                foreach (var rawDeclaration in Namespaces.Split(';'))
                {
                    var namespaceDeclaration = rawDeclaration.Trim();
                    var index = namespaceDeclaration.IndexOf("=");
                    if (index < 0)
                        throw new ArgumentException("Namespaces must be of the form \"ns1=uri1;ns2=uri2;...\"",
                            NamespacesAttribute);
                    var prefix = namespaceDeclaration.Substring(0, index);
                    var uri = namespaceDeclaration.Substring(index + 1);
                    namespaceResolver.AddNamespace(prefix, uri);
                }
            }
            return namespaceResolver;
        }
    }
}

可以在App.config中使用,如下所示。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <extensions>
      <behaviorExtensions>
        <add name="redeclareNamespaces"
             type="RedeclareNamespaces.RedeclareNamespacesConfigurationSection,RedeclareNamespaces"/>
      </behaviorExtensions>
    </extensions>
    <behaviors>
      <endpointBehaviors>
        <behavior name="RedeclareNamespacesBehavior">
          <redeclareNamespaces actionFilter="http://myExampleUri/IExampleService/Frobnicate"
                               namespaces="soapenv=http://schemas.xmlsoap.org/soap/envelope/;
                                           myNamespace=http://myExampleUri"
                               targetElements="/soapenv:Envelope/soapenv:Body//myNamespace:Frobnicate/myNamespace:xamlArgumentHere/*"/>
        </behavior>
      </endpointBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>

不幸的是,消息检查器必须至少复制整个信封两次:首先必须将Message复制到缓冲区,然后在转换后必须将XDocument保存到流中。如果我直接将document.CreateReader()传递给Message构造函数,由于XML节点流无效,我遇到了一些令人困惑的例外。