2016-06-30 5 views
0

Ich versuche, mehrere XAML-Dokumentfragmente an einen SOAP basicHttpBinding Web-Service als XElement Instanzen übergeben.Zugriff auf XML-Präfixe in WCF

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

Die Nachrichten enden mit einem Format wie unten.

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

Alle gut ist myPrefix auf der Wurzel der XAML Insel definiert ist (d.h. <SomeObject>). wenn ich die Namespace-Deklaration zum <soapenv:Envelope> jedoch bewegen (was offensichtlich erlaubt, vorbehältlich Präfix Auseinandersetzungen)

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

Das resultierende XElement nicht mehr Wissen über, dass die URI yetAnotherNamespace wird durch das Präfix tatsächlich repräsentiert myPrefix. Stattdessen wird ein generisches Präfix (p2) zugewiesen.

Daher XamlXmlReader Lage sein wird, die Markup-Erweiterung {myPrefix:Foo}, zu lesen, da sie noch mit dem alten Präfix myPrefix statt p2 zugeordnet ist. Für die XML-Verarbeitung innerhalb von WCF war die Markuperweiterung tatsächlich ein Zeichenfolgenattribut und erhielt keine spezifische Behandlung für Namespacepräfixe.

Das Verhalten, in dem das erfolgreiche Lesen der XML-Insel von der Platzierung der Präfixdeklarationen xmlns: abhängt, ist höchst verwirrend. Gibt es eine Möglichkeit, Präfixe irgendwie neu zu ordnen, um das Verhalten zu verbessern?

Antwort

0

Spielen ein wenig herum erkannte ich, dass, wenn ich XmlElement statt XElement verwenden, das Problem verschwindet, da die Original-Präfixe sind alle gehalten. Allerdings bin ich noch gespannt, ob eine elegantere Lösung besteht ...

Leider System.Xml.Linq-System.Xml zurückkehrt hilft nicht, denn wenn ein Namespacepräfix nur in Markup-Erweiterungen verwendet wird (nicht-Tags), dessen Präfix wird unwiederbringlich sein. Stattdessen habe ich einen IDispatchMessageInspector implementiert, der die Namespaces für ausgewählte XML-Elemente innerhalb des SOAP-Umschlags neu deklariert (erbt xmlns: Deklarationen von der gesamten Hüllkurve), um ein ausgewähltes XML-Fragment eigenständig zu machen.

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

Es kann in App.config wie folgt verwendet werden.

<?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> 

Leider hat die Nachricht Inspektor den gesamten Umschlag mindestens zweimal kopieren: zuerst die Message in einen Puffer kopiert werden muss, dann nach der Transformation der XDocument muss in den Strom gespart werden. Ich habe einige verblüffende Ausnahmen aufgrund eines ungültigen XML-Node-Streams erhalten, wenn ich document.CreateReader() direkt an den Message-Konstruktor übergeben habe.