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+"=");
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:
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With