clockwiseq
clockwiseq

Reputation: 4229

XDocument Save Removing Node Prefixes

I have an XML document (homegrown) that has a structure like the following:

<?xml version="1.0" encoding="utf-8"?>
    <wf:wf version="1.0a" xmlns:wf="http://example.com/workflow" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://example.com/workflow">
  <wf:assemblies />
  <wf:dataDefinitions />
  <wf:processes />
  <wf:workflows>
    <wf:workflow id="g08615517-cdfd-4091-a053-217a965f7118">
      <wf:arguments />
      <wf:variables>
        <wf:variable id="g39ffecc9-f570-41c1-9ee0-b9358d63da3c" parameterType="Hidden">
          <wf:name>UIPTaskId</wf:name>
          <wf:dataDefinitionId>gc8f3715c-4a82-42d2-916c-51515083e7e5</wf:dataDefinitionId>
        </wf:variable>
        <wf:variable id="g46663a0c-7e60-4bd2-80df-16cd544087ad" parameterType="Hidden">
          <wf:name>UIPTaskName</wf:name>
          <wf:dataDefinitionId>g359FC555-9CC7-47D4-8ED3-EF973E7D74D7</wf:dataDefinitionId>
          <wf:value>Responsible Individual</wf:value>
        </wf:variable>
        <wf:variable id="gb32914d5-6761-4e82-b571-c8944a796fd9" parameterType="Hidden">
          <wf:name>Search?</wf:name>
          <wf:dataDefinitionId>g57201da8-62b4-46f2-9329-c71d86f39ffc</wf:dataDefinitionId>
          <wf:value>True</wf:value>
        </wf:variable>
    </wf:variables>
</wf:workflow>
</wf:workflows>
</wf:wf>

I have a utility to clean up the XML documents and am using XDocument to load the file and then loop through certain nodes and replace values. Once done, I am calling the Save method to save the file in a new location and upon further inspection, the Save method is removing my wf prefix on every node. How can I preserve this? Am I doing something wrong? Here is a sample of my code:

string wf = "wf";
string wkfl = "C:\\MyFiles\\Temp\\myfile1.rrr";

XDocument xdoc = XDocument.Load(wkfl);
XElement variables= xdoc.Descendents(wf + "variables").Single();

foreach(XElement variable in variables.Elements(wf + "variable"))
{
    XElement name = variable.Element(wf + "name");
    name.Value = name.Value + "_MODIFIED";  
}

xdoc.Save(wkfl.Replace("\\Temp\\", "\\Modified\\"));

The Save method produces the following XML:

<?xml version="1.0" encoding="utf-8"?>
        <wf version="1.0a" xmlns:wf="http://example.com/workflow" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://example.com/workflow">
      <assemblies />
      <dataDefinitions />
      <processes />
      <workflows>
        <workflow id="g08615517-cdfd-4091-a053-217a965f7118">
          <arguments />
          <variables>
            <variable id="g39ffecc9-f570-41c1-9ee0-b9358d63da3c" parameterType="Hidden">
              <name>UIPTaskId</name>
              <dataDefinitionId>gc8f3715c-4a82-42d2-916c-51515083e7e5</dataDefinitionId>
            </variable>
            <variable id="g46663a0c-7e60-4bd2-80df-16cd544087ad" parameterType="Hidden">
              <name>UIPTaskName</name>
              <dataDefinitionId>g359FC555-9CC7-47D4-8ED3-EF973E7D74D7</dataDefinitionId>
              <value>Responsible Individual</value>
            </variable>
            <variable id="gb32914d5-6761-4e82-b571-c8944a796fd9" parameterType="Hidden">
              <name>Search?</name>
              <dataDefinitionId>g57201da8-62b4-46f2-9329-c71d86f39ffc</dataDefinitionId>
              <value>True</value>
            </variable>
        </variables>
    </workflow>
    </workflows>
    </wf>

Upvotes: 1

Views: 1648

Answers (1)

dbc
dbc

Reputation: 117155

This behavior can be reproduced simply by loading your XML and writing it again without making any edits. Doing:

        var xdoc = XDocument.Parse(xml);
        Debug.WriteLine(xdoc.ToXml());

Produces the output:

<wf version="1.0a" xmlns:wf="http://example.com/workflow" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://example.com/workflow">
  <assemblies />
  <dataDefinitions />
  <processes />
  <workflows>
      ...

using the helper method:

public static class XmlSerializationHelper
{
    public static string ToXml(this XDocument xDoc)
    {
        using (TextWriter writer = new StringWriter())
        {
            xDoc.Save(writer);
            return writer.ToString();
        }
    }
}

Why is this happening?

  1. You have two namespaces with identical values, the default namespace and the namespace with prefix wf:

    xmlns="http://example.com/workflow"
    xmlns:wf="http://example.com/workflow"
    
  2. Thus the prefix wf: means exactly the same thing as having no prefix at all for the wf element and all child elements.

  3. Thus, when writing itself back to XML, XElement could validly use the prefix wf:, or use no prefix at all, without changing the semantic meaning of the output XML.

  4. So how does XElement chose between multiple valid prefixes? As it turns out, from the reference source for XElement, 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.

  5. Thus your XElements are given the second of the two possible valid prefixes -- namely no prefix.

To summarize, the XML with the prefixes and the XML without the prefixes are semantically identical. No proper XML parser should care about the difference.

Nevertheless, if for whatever reason some code you are working with assumes the wf: prefix rather than checking the actual namespace name (though it shouldn't), you can force your XML to be written out with that prefix by reordering the default namespace to the beginning of the root document attribute list:

    public static void ReorderDefaultNamespaceToBeginning(XElement xElement)
    {
        var attrArray = xElement.Attributes().ToArray();

        int defaultIndex = -1;
        for (int i = 0; i < attrArray.Length && defaultIndex == -1; i++)
        {
            var attr = attrArray[i];
            if (attr.Name == XName.Get("xmlns", string.Empty))
                defaultIndex = i;
        }

        if (defaultIndex < 0)
            return; // No default namespace

        int firstIndex = -1;
        for (int i = 0; i < attrArray.Length && firstIndex == -1; i++)
        {
            if (i == defaultIndex)
                continue;
            var attr = attrArray[i];
            if (attr.Name.NamespaceName == "http://www.w3.org/2000/xmlns/"
                && attr.Value == attrArray[defaultIndex].Value)
                firstIndex = i;
        }

        if (defaultIndex != -1 && firstIndex != -1 && defaultIndex > firstIndex)
        {
            foreach (var attr in attrArray)
                attr.Remove();
            attrArray.Swap(defaultIndex, firstIndex);
            foreach (var attr in attrArray)
                xElement.Add(attr);
        }
    }

public static class ListHelper
{
    public static void Swap<T>(this T[] list, int i, int j)
    {
        if (i != j)
        {
            T temp = list[i];
            list[i] = list[j];
            list[j] = temp;
        }
    }
}

(This takes advantage of the undocumented fact that namespace prefixes are checked in reverse order of appearance.) Once you do this the wf: prefixes will return.

Upvotes: 6

Related Questions