1. Introducción
Imagínate un XML gigantesco que te envía algún servicio SOAP o un API bancario. Tiene cientos de elementos anidados, estructuras heterogéneas, y necesitas encontrar solo una única suma de pago del mes pasado. La técnica de serialización no sirve aquí — es demasiado pesada y no sabes de antemano dónde mirar. Hace falta una "lupa" para el árbol XML y la habilidad de moverse rápido por sus ramas.
Justo para estos casos en .NET existe desde hace años una herramienta potente — la clase XmlDocument, complementada por el lenguaje de consultas XPath. Hoy aprenderás a:
- Cargar XML en un árbol DOM;
- Moverte y encontrar elementos a mano y usando XPath;
- Modificar el contenido de un documento XML;
- Añadir, eliminar y cambiar nodos;
- Usar XPath para selecciones complejas.
2. ¿Qué es XmlDocument?
XmlDocument es una clase del namespace System.Xml. Es una implementación del modelo DOM (Document Object Model) — la representación de todo el archivo XML como un árbol de objetos en memoria.
Un poco de teoría y analogías
Si XmlSerializer se parece a un constructor LEGO, cuando conviertes un objeto en un conjunto de piezas y viceversa, entonces XmlDocument es como trabajar con un árbol real: tiene una raíz, ramas (elementos), hojas (nodos de texto), y puedes recorrer ese árbol, añadir ramas, cortar hojas y trasplantarlas donde quieras.
Carga simple de un documento XML
Supongamos que tenemos este XML:
<users>
<user id="1">
<name>Olga</name>
<age>28</age>
</user>
<user id="2">
<name>Igor</name>
<age>35</age>
</user>
</users>
Cargémoslo en memoria:
using System.Xml;
string xml = @"
<users>
<user id='1'>
<name>Olga</name>
<age>28</age>
</user>
<user id='2'>
<name>Igor</name>
<age>35</age>
</user>
</users>";
XmlDocument doc = new XmlDocument();
doc.LoadXml(xml); // o doc.Load("ruta_al_archivo.xml");
Después de llamar a LoadXml (o Load, si el XML viene de un archivo) tienes acceso completo al contenido del documento.
3. Navegación por el árbol DOM
Estructura del árbol DOM
Cada documento XML tras cargarse se convierte en un árbol compuesto por nodos de distintos tipos:
| Tipo de nodo | Clase en .NET | Ejemplo |
|---|---|---|
| Document | XmlDocument | |
| Element | XmlElement | |
| Attribute | XmlAttribute | |
| Text | XmlText | Olga, 28 |
| Comment | XmlComment | |
Para acceder a los elementos necesarios tendrás que "caminar por el árbol" usando propiedades como ChildNodes, Attributes, ParentNode, etc.
Obtenemos el elemento raíz
XmlElement root = doc.DocumentElement;
Console.WriteLine(root.Name); // users
Iteramos elementos hijos
foreach (XmlNode node in root.ChildNodes)
{
if (node is XmlElement user)
{
// user — es <user id="...">...</user>
string id = user.GetAttribute("id");
string name = user["name"].InnerText;
string age = user["age"].InnerText;
Console.WriteLine($"Usuario {id}: {name}, edad {age}");
}
}
IMPORTANTE: El acceso user["name"] es posible solo si entre los hijos directos existe el elemento <name>.
Acceso a atributos y texto
var firstUser = root.FirstChild as XmlElement;
string id = firstUser.GetAttribute("id"); // "1"
string name = firstUser["name"].InnerText; // "Olga"
4. Modificación del documento XML
Cambiar un valor
Supongamos que Olga de repente decidió "rejuvenecer":
var olga = root.FirstChild as XmlElement;
olga["age"].InnerText = "22"; // ahora <age>22</age>
Añadir un nuevo usuario
XmlElement newUser = doc.CreateElement("user");
newUser.SetAttribute("id", "3");
XmlElement name = doc.CreateElement("name");
name.InnerText = "Vasilisa";
XmlElement age = doc.CreateElement("age");
age.InnerText = "19";
newUser.AppendChild(name);
newUser.AppendChild(age);
root.AppendChild(newUser);
Eliminar un elemento
Eliminemos al segundo usuario ("Igor"):
XmlNode userToDelete = root.SelectSingleNode("user[@id='2']");
if (userToDelete != null)
root.RemoveChild(userToDelete);
Guardar cambios
doc.Save("users_updated.xml");
// O doc.OuterXml — para obtener la cadena con el XML
5. XPath — lenguaje de búsqueda y selección en XML
Trabajar con el árbol DOM se vuelve pesado si necesitas buscar elementos "por condición" — por ejemplo, todos los usuarios mayores de 25 años. Para esto existe XPath — un lenguaje para navegar por el árbol XML.
Uso básico de XPath
Las consultas XPath se ejecutan con los métodos SelectSingleNode (devuelve el primer nodo encontrado) y SelectNodes (devuelve una colección de nodos).
Ejemplo: encontrar el usuario con id = 1
XmlNode user = root.SelectSingleNode("user[@id='1']");
Console.WriteLine(user["name"].InnerText); // Olga
Ejemplo: encontrar todos los usuarios mayores de 25 años
XmlNodeList nodes = root.SelectNodes("user[age>25]");
foreach (XmlNode u in nodes)
{
Console.WriteLine(u["name"].InnerText); // Mostrará Olga e Igor (si Igor aún no fue eliminado)
}
XPath — sintaxis breve
| Expresión XPath | Qué hace |
|---|---|
|
Todos los elementos <user> en la raíz <users> |
|
Usuario con id=3 |
|
Usuarios mayores de 25 años |
|
Todos los elementos <name> de todos los usuarios |
|
El último <user> |
|
Los dos primeros <user> |
Más ejemplos y sintaxis: Documentación de XPath.
Otro ejemplo: selección por elementos anidados
Supongamos que el XML es más complejo:
<library>
<book>
<title>El señor de las moscas</title>
<author>
<firstname>William</firstname>
<lastname>Golding</lastname>
</author>
</book>
<book>
<title>A la sombra de las muchachas en flor</title>
<author>
<firstname>Marcel</firstname>
<lastname>Proust</lastname>
</author>
</book>
</library>
XmlDocument doc = new XmlDocument();
doc.LoadXml(libraryXml);
XmlNodeList authors = doc.SelectNodes("/library/book/author/lastname");
foreach (XmlNode lastName in authors)
Console.WriteLine(lastName.InnerText);
Qué se imprimirá:
Golding
Proust
6. XPath: filtrado, lógica, cálculos
Filtros lógicos
Imprimir los nombres de todos los usuarios cuya edad está entre 18 y 30:
// [age>=18 and age<=30]
XmlNodeList youngUsers = root.SelectNodes("user[age>=18 and age<=30]");
foreach (XmlNode u in youngUsers)
Console.WriteLine(u["name"].InnerText);
Trabajo con atributos
Los atributos se seleccionan con @. Encontraremos usuarios cuyo id empieza por "1":
XmlNodeList nodes = root.SelectNodes("user[starts-with(@id, '1')]");
Contar nodos
Directamente en el método C# SelectNodes no puedes usar la función count() para obtener un número — devuelve una colección, no un número. Pero puedes hacer:
int count = root.SelectNodes("user").Count;
Filtros anidados
XmlNodeList nodes = doc.SelectNodes("/library/book[author/lastname='Golding']");
7. XPath y espacios de nombres
Si tu XML usa espacios de nombres (xmlns), todo se complica un poco. Para esos casos usa XmlNamespaceManager:
<catalog xmlns="http://books.example.com">
<book>
<title>Algoritmos</title>
</book>
</catalog>
doc.LoadXml(xml);
XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable);
nsmgr.AddNamespace("b", "http://books.example.com");
XmlNodeList books = doc.SelectNodes("/b:catalog/b:book", nsmgr);
foreach (XmlNode book in books)
Console.WriteLine(book.SelectSingleNode("b:title", nsmgr)?.InnerText);
8. Modificar XML con DOM
Añadir nodos "sobre la marcha"
Añadiremos un nuevo elemento <email> al primer usuario:
XmlElement email = doc.CreateElement("email");
email.InnerText = "olga@gmail.com";
olga.AppendChild(email);
Cambiar atributos
olga.SetAttribute("id", "99"); // Ahora id="99"
Eliminar nodos usando XPath
XmlNodeList nodesToRemove = root.SelectNodes("user[age<20]");
foreach (XmlNode node in nodesToRemove)
root.RemoveChild(node);
9. Novedades útiles
Búsqueda y modificación masiva de estructura
Supongamos que recibes un XML grande con distintos elementos <record type="...">. Necesitas dejar solo aquellos cuyo atributo type sea "customer", y añadir a cada uno un hijo <status>active</status>.
XmlNodeList customerNodes = root.SelectNodes("record[@type='customer']");
foreach (XmlElement record in customerNodes)
{
XmlElement status = doc.CreateElement("status");
status.InnerText = "active";
record.AppendChild(status);
}
Árbol de nodos XML (DOM)
users
├─ user (id="1")
│ ├─ name ("Olga")
│ └─ age ("28")
├─ user (id="2")
│ ├─ name ("Igor")
│ └─ age ("35")
Ejemplo de selección con XPath
| Consulta XPath | Qué devolverá |
|---|---|
|
El primer usuario (id="1") |
|
El último usuario (id="2") |
|
Todos los mayores de 30 años (Igor) |
|
Usuario con id="2" |
Aplicación práctica
- Integraciones con APIs "antiguas". En muchas corporaciones y bancos los servicios SOAP/XML aún dominan. Poder encontrar y cambiar rápido algo en una gran respuesta XML puede ahorrar decenas de horas.
- Migración de datos. Al pasar de un sistema a otro a menudo hay que parsear XML y hacer selecciones complejas, transformaciones y cambios masivos.
- Importación-exportación a Excel. En muchos productos B2B la entrada sigue siendo XML. Ahí XPath es una forma rápida de sacar lo necesario sin crear un modelo de datos grande.
10. Errores, matices y trampas típicas
NullReferenceException: Si intentas acceder a un elemento inexistente, por ejemplo el["something"].InnerText, y ese hijo no existe en absoluto. Revisa siempre por null.
XPath y anidamiento: Recuerda que una expresión sin la barra inicial / busca entre los descendientes del nodo actual, mientras que con la / — desde la raíz del documento. Diferentes puntos de referencia pueden producir ausencia de resultados aunque los datos existan.
Trabajo con espacios de nombres: Si no usas XmlNamespaceManager, las consultas XPath contra XML con xmlns devolverán vacío.
Modificar durante la iteración: Si quieres eliminar nodos resultantes de SelectNodes, primero guárdalos en un array y luego itera — si no, la colección cambia durante la iteración y pueden surgir errores.
Diferencia entre nodos de texto y nodos de elemento: Entre elementos puede haber nodos de texto — espacios y saltos de línea que XML considera contenido. Para selecciones estrictas usa solo XmlElement o filtra por NodeType.
GO TO FULL VERSION