views:

274

answers:

2

I have an XDocument that looks similar to

<root>
     <a>
          <b foo="1" bar="2" />
          <b foo="3" bar="4" />
          <b foo="5" bar="6" />
          <b foo="7" bar="8" />
          <b foo="9" bar="10" />
     </a>
</root>

I wish to change the attribute foo to something else, and the attribute bar to something else. How can I easily do this? My current version (below) stack overflows with large documents, and has an awful smell to it.

        string dd=LoadedXDocument.ToString();
        foreach (var s in AttributeReplacements)
            dd = dd.Replace(s.Old+"=", s.New+"=");
+1  A: 

Doing this with a text search and replacement should be done using StringBuilder to avoid the normal issues of creating strings in a loop (lots of garbage). It also is very hard to prevent false positives (what if text matching the attribute occurs in a text node?)

Better options, with different tradeoffs include:

  1. Load into XDocument or XmlDocument, iterate through the tree replacing matching attributes.
  2. Use an XSLT
  3. Read from an XmlReader and write directly to an XmlWriter, with changed attributes.

Of these #3 avoids loading the whole document into memory. #2 requires XSLT skills but easily allows an arbitrary number of replacements (the core of the XSLT could be a template, with the new,old attribute pairs injected at runtime). #1 is likely to be simplest, but with the whole document in memory, and overhead of handling multiple replacements.

I would likely look at XSLT with Xml Reader/Writer approach as a backup.

However #1 should be simplest to implement, something like (ignoring XML namespaces amongst other details):

using System.Xml.Linq;
using System.Xml.XPath;

var xdoc = XDocument.Load(....);
var nav = xdoc.CreateNavigator();

foreach (repl in replacements) {
  var found = (XPathNodeIterator) nav.Evaluate("//@" + repl.OldName);

  while (found.MoveNext()) {
    var node = found.Current;
    var val = node.Value;
    node.DeleteSelf(); // Moves ref to parent.
    node.CreateAttribute("", repl.NewName, "", val);
  }
}

The final choice will depend with balancing performance (especially memory if working with large documents) and complexity. but only you (and your team) can make that call.

Richard
+1  A: 

Here is a complete XSLT solution:

<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:my="my:reps"
    exclude-result-prefixes="my"
>
    <xsl:output omit-xml-declaration="yes" indent="yes"/>

    <my:replacements>
      <foo1 old="foo"/>
      <bar1 old="bar"/>
    </my:replacements>

    <xsl:variable name="vReps" select=
     "document('')/*/my:replacements/*"/>

 <xsl:template match="node()|@*">
     <xsl:copy>
       <xsl:apply-templates select="node()|@*"/>
     </xsl:copy>
 </xsl:template>

 <xsl:template match="@*">
  <xsl:variable name="vRepNode" select=
   "$vReps[@old = name(current())]"/>

   <xsl:variable name="vName" select=
    "name(current()[not($vRepNode)] | $vRepNode)"/>

   <xsl:attribute name="{$vName}">
     <xsl:value-of select="."/>
   </xsl:attribute>
 </xsl:template>
</xsl:stylesheet>

When this transformation is applied on the provided XML document, the desired result is produced:

<root>
   <a>
      <b foo1="1" bar1="2"/>
      <b foo1="3" bar1="4"/>
      <b foo1="5" bar1="6"/>
      <b foo1="7" bar1="8"/>
      <b foo1="9" bar1="10"/>
   </a>
</root>

Do note that this is a generic solution, allowing any list of replacements to be specified and modified without modifying the code. The replacements can be in a separate XML file, for ease of maintenance.

Dimitre Novatchev
Thank you very much for your efforts.
Dested