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