XDocument 具有不同本地名称的重复命名空间

XDocument duplicate namespace with different Local Name

我有一个 XML 文档,如下所示:

    <Schema Namespace="BBSF_Model" Alias="Self"
      p1:UseStrongSpatialTypes="false"
      xmlns:annotation="http://schemas.microsoft.com/ado/2009/02/edm/annotation"
      xmlns:p1="http://schemas.microsoft.com/ado/2009/02/edm/annotation"
      xmlns="http://schemas.microsoft.com/ado/2009/11/edm">
    <EntityType Name="Customer">
        <Property Name="Id" Type="Guid" Nullable="false" />
    </EntityType>
  </Schema>

我使用下面的代码修改了文档的 属性 元素:

    XElement csdlEentity = csdlDoc.Root.Descendants()
            .Where(d => d.Name.LocalName == "EntityType")
            .FirstOrDefault(e => e.Attribute("Name").Value == "Customer");

    var csdlProperty = csdlEentity.Descendants()
            .Where(d => d.Name.LocalName == "Property")
            .FirstOrDefault(e => e.Attribute("Name").Value == "Id");

    XNamespace annotation = "http://schemas.microsoft.com/ado/2009/02/edm/annotation";
    var attrib = new XAttribute(annotation + "StoreGeneratedPattern", "Computed");
    csdlProperty.Add(attrib);

当我保存 XDocument 时,属性 元素看起来像:

    <Property Name="Id" Type="Guid" Nullable="false" p1:StoreGeneratedPattern="Computed" />

然而我想要的是:

  <Property Name="Id" Type="Guid" Nullable="false" annotation:StoreGeneratedPattern="Computed" />

问题是xmlns“http://schemas.microsoft.com/ado/2009/02/edm/annotation”在文档的根节点中被引用了两次,有两个不同的aliases/LocalNames(注释,p1)

    xmlns:annotation="http://schemas.microsoft.com/ado/2009/02/edm/annotation"
    xmlns:p1="http://schemas.microsoft.com/ado/2009/02/edm/annotation"

我无法更改或篡改根节点。

如何保存文档或更新 属性 元素以提供所需的输出?

理论上,使用前缀 p1:StoreGeneratedPattern="Computed"annotation:StoreGeneratedPattern="Computed" 应该无关紧要,因为它们的意思 完全相同 -- expanded XML name of {http://schemas.microsoft.com/ado/2009/02/edm/annotation}StoreGeneratedPattern. If your receiving XML parser (or QA department?) has a problem dealing with this, the easiest fix may be to fix the parser to conform with the standard.

元素

话虽这么说,来自 reference source for XElement, it turns out that the namespace/prefix attribute pairs are pushed onto a push-down stack in order of addition while writing, then checked for matches against the element namespace from top to bottom of the stack -- effectively doing the match in reverse order in which the attributes are added. So if you wanted to use the prefix annotation, you could simply permute the order of duplicated namespaces in the root element. (For details, see 。)

但是,您写道您无法更改或篡改根节点。因此,您将被迫做一些工作:您将需要创建自己的 XmlWriter 包装器子类并自己进行前缀重映射。

首先,XmlWriter 的非抽象子类包装了 "real" XmlWriter:

public class XmlWriterProxy : XmlWriter
{
    readonly XmlWriter baseWriter;

    public XmlWriterProxy(XmlWriter baseWriter)
    {
        if (baseWriter == null)
            throw new ArgumentNullException();
        this.baseWriter = baseWriter;
    }

    protected virtual bool IsSuspended { get { return false; } }

    public override void Close()
    {
        baseWriter.Close();
    }

    public override void Flush()
    {
        baseWriter.Flush();
    }

    public override string LookupPrefix(string ns)
    {
        return baseWriter.LookupPrefix(ns);
    }

    public override void WriteBase64(byte[] buffer, int index, int count)
    {
        if (IsSuspended)
            return;
        baseWriter.WriteBase64(buffer, index, count);
    }

    public override void WriteCData(string text)
    {
        if (IsSuspended)
            return;
        baseWriter.WriteCData(text);
    }

    public override void WriteCharEntity(char ch)
    {
        if (IsSuspended)
            return;
        baseWriter.WriteCharEntity(ch);
    }

    public override void WriteChars(char[] buffer, int index, int count)
    {
        if (IsSuspended)
            return;
        baseWriter.WriteChars(buffer, index, count);
    }

    public override void WriteComment(string text)
    {
        if (IsSuspended)
            return;
        baseWriter.WriteComment(text);
    }

    public override void WriteDocType(string name, string pubid, string sysid, string subset)
    {
        if (IsSuspended)
            return;
        baseWriter.WriteDocType(name, pubid, sysid, subset);
    }

    public override void WriteEndAttribute()
    {
        if (IsSuspended)
            return;
        baseWriter.WriteEndAttribute();
    }

    public override void WriteEndDocument()
    {
        if (IsSuspended)
            return;
        baseWriter.WriteEndDocument();
    }

    public override void WriteEndElement()
    {
        if (IsSuspended)
            return;
        baseWriter.WriteEndElement();
    }

    public override void WriteEntityRef(string name)
    {
        if (IsSuspended)
            return;
        baseWriter.WriteEntityRef(name);
    }

    public override void WriteFullEndElement()
    {
        if (IsSuspended)
            return;
        baseWriter.WriteFullEndElement();
    }

    public override void WriteProcessingInstruction(string name, string text)
    {
        if (IsSuspended)
            return;
        baseWriter.WriteProcessingInstruction(name, text);
    }

    public override void WriteRaw(string data)
    {
        if (IsSuspended)
            return;
        baseWriter.WriteRaw(data);
    }

    public override void WriteRaw(char[] buffer, int index, int count)
    {
        if (IsSuspended)
            return;
        baseWriter.WriteRaw(buffer, index, count);
    }

    public override void WriteStartAttribute(string prefix, string localName, string ns)
    {
        if (IsSuspended)
            return;
        baseWriter.WriteStartAttribute(prefix, localName, ns);
    }

    public override void WriteStartDocument(bool standalone)
    {
        baseWriter.WriteStartDocument(standalone);
    }

    public override void WriteStartDocument()
    {
        baseWriter.WriteStartDocument();
    }

    public override void WriteStartElement(string prefix, string localName, string ns)
    {
        if (IsSuspended)
            return;
        baseWriter.WriteStartElement(prefix, localName, ns);
    }

    public override WriteState WriteState
    {
        get { return baseWriter.WriteState; }
    }

    public override void WriteString(string text)
    {
        if (IsSuspended)
            return;
        baseWriter.WriteString(text);
    }

    public override void WriteSurrogateCharEntity(char lowChar, char highChar)
    {
        if (IsSuspended)
            return;
        baseWriter.WriteSurrogateCharEntity(lowChar, highChar);
    }

    public override void WriteWhitespace(string ws)
    {
        if (IsSuspended)
            return;
        baseWriter.WriteWhitespace(ws);
    }
}

接下来,允许重新映射属性命名空间前缀的子类:

public class PrefixSelectingXmlWriterProxy : XmlWriterProxy
{
    readonly Stack<XName> elements = new Stack<XName>();
    readonly Func<string, string, string, Stack<XName>, string> attributePrefixMap;

    public PrefixSelectingXmlWriterProxy(XmlWriter baseWriter, Func<string, string, string, Stack<XName>, string> attributePrefixMap)
        : base(baseWriter)
    {
        if (attributePrefixMap == null)
            throw new NullReferenceException();
        this.attributePrefixMap = attributePrefixMap;
    }

    public override void WriteStartAttribute(string prefix, string localName, string ns)
    {
        prefix = attributePrefixMap(prefix, localName, ns, elements);
        base.WriteStartAttribute(prefix, localName, ns);
    }

    public override void WriteStartElement(string prefix, string localName, string ns)
    {
        base.WriteStartElement(prefix, localName, ns);
        elements.Push(XName.Get(localName, ns));
    }

    public override void WriteEndElement()
    {
        base.WriteEndElement();
        elements.Pop(); // Pop after base.WriteEndElement() lets the base class throw an exception on a stack error.
    }
}

最后你会像这样使用它:

        string xml;
        using (var sw = new StringWriter())
        {
            using (var xmlWriter = XmlWriter.Create(sw, new XmlWriterSettings { Indent = true, IndentChars = "  " }))
            using (var xmlWriterProxy = new PrefixSelectingXmlWriterProxy(xmlWriter,
                (string prefix, string localName, string ns, Stack<XName> parents) =>
                {
                    if (localName == "StoreGeneratedPattern" && ns == annotation && parents.Peek() == XName.Get("Property", "http://schemas.microsoft.com/ado/2009/11/edm"))
                        return "annotation";
                    return prefix;
                })
                )
            {
                csdlDoc.WriteTo(xmlWriterProxy);
            }
            xml = sw.ToString();
        }
        Debug.WriteLine(xml);

如您所见,这仅重新映射属性前缀,但显然可以通过覆盖 WriteStartElement(string prefix, string localName, string ns).

将其扩展为重新映射元素前缀

工作fiddle

多亏了你的想法,我找到了解决方案。 一般的想法是我将 p1 的值更改为任何添加我的新属性然后甚至在保存 XDocument 之前将 p1 返回到其原始值。

XAttribute p1 = csdlDoc.Root.Attributes()
            .First(a => a.IsNamespaceDeclaration && a.Name.LocalName == "p1");

p1.Value = "http://schemas.microsoft.com/ado/2009/02/edm/annotation/TEMP";

......
// the same update now works as there is only one xmlns 
XNamespace annotation = "http://schemas.microsoft.com/ado/2009/02/edm/annotation";
var attrib = new XAttribute(annotation + "StoreGeneratedPattern", "Computed");

csdlProperty.Add(attrib);

p1.Value = "http://schemas.microsoft.com/ado/2009/02/edm/annotation/";

另一种有效的解决方案是在根节点交换顺序或声明(在注释之前声明 p1),如下所示:

<Schema Namespace="BBSF_Model" Alias="Self"
  p1:UseStrongSpatialTypes="false"
  xmlns:p1="http://schemas.microsoft.com/ado/2009/02/edm/annotation"
  xmlns:annotation="http://schemas.microsoft.com/ado/2009/02/edm/annotation" 
  xmlns="http://schemas.microsoft.com/ado/2009/11/edm">

总的来说,两者看起来都很便宜..