【问题标题】:Access to XML prefixes in WCF在 WCF 中访问 XML 前缀
【发布时间】:2016-06-30 20:50:14
【问题描述】:

我正在尝试将几个 XAML 文档片段作为 XElement 实例传递给 SOAP basicHttpBinding 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 岛的根上(即&lt;SomeObject&gt;)。但是,如果我将命名空间声明移动到 &lt;soapenv:Envelope&gt;(这显然是允许的,除非有任何前缀冲突)

<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: 前缀声明的位置,这种行为非常令人困惑。 有没有办法以某种方式重新映射前缀以获得更正确的行为?

【问题讨论】:

    标签: xml wcf xaml soap


    【解决方案1】:

    玩了一下,我意识到如果我使用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 节点流而出现一些令人费解的异常。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2022-08-09
      • 1970-01-01
      • 2023-03-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多