Welcome to Comunidad .NET de Cd. Juárez Sign in | Join | Help

Browse by Tags

All Tags » linq » herramientas

Cómo transformar XML sin usar XSLT

El otro día, una compañía externa hizo un una auditoría de seguridad a un sistema grande que manejamos.  Parte de la auditoría consistió en correr la herramienta FxCop contra las DLLs para buscar—entre otras cosas—posibles vulnerabilidades de inyección de SQL.  FxCop produce dichos reportes en XML y eso fue lo que nos entregaron. 

Estos reportes eran enormes y en un formato no muy amigable, así que necesitábamos de alguna forma extraer los datos de ahí para enfocar los esfuerzos de los desarrolladores (qué corregir y en dónde), y también para tabular los datos (número de issues por namespace, DLL, etc.) de manera que se les pudieran presentar a los gerentoides. 

Este es un ejemplo de uno de esos archivos—no te preocupes, todas estas vulnerabilidades ya fueron parchadas, este es un extracto de esos reportes smile_wink.

(Da clic a la imagen para ver un tamaño más grande)

ejemplo de reporte FxCop

Ahora, se me ocurrió que estos archivos se podían transformar a algo menos plano—como otro archivo de XML o un CSV—de manera que pudiera ser importado a Excel fácilmente.  Algo así como lo siguiente:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Vulnerabilities>
  <Vulnerability>
    <Module></Module>
    <Namespace></Namespace>
    <Type></Type>
    <Method></Method>
    <Issue></Issue>
  </Vulnerability>
  <Vulnerability>
    <Module></Module>
    <Namespace></Namespace>
    <Type></Type>
    <Method></Method>
    <Issue></Issue>
  </Vulnerability>
</Vulnerabilities>

La sola idea de hacer una hoja de estilos XSL para transformar eso me daba escalofríos, así que fue una buena oportunidad para practicar mi LINQ-Fu.  Para ello me ayudé de LINQPad—una excelente herramienta para probar snippets de LINQ.

Armando el query

Mi primer query fue este:

XElement xml = XElement.Load(@"C:\Users\Carlos\Desktop\sampleFxCopReport.xml");
 
var query =
    from e in xml.Descendants("Issue")
    where e.Ancestors("Message").Attributes("TypeName").First().Value == "ReviewSqlQueriesForSecurityVulnerabilities"
    select new
    {
        Module = e.Ancestors("Module").Attributes("Name").First().Value,
        Namespace = e.Ancestors("Namespace").Attributes("Name").First().Value,
        Type = e.Ancestors("Type").Attributes("Name").First().Value,
        Method = e.Ancestors("Member").Attributes("Name").First().Value,
        Issue = e.Value
    };

Como puedes ver, cargar el archivo es trivial.  Puedes hacerlo a través del método Load() de la clase XElement o XDocument.

Luego, declaré una variable de tipo var porque no estaba seguro exactamente del tipo de los objetos que regresaría mi query—utilicé un tipo anónimo, el cual explicaré en un minuto.

Si no estás muy familiarizado con queries de LINQ, solo recuerda que en su nivel más básico, tienes un from/where/select

  • Primero le dices a qué objetos quieres hacerle query (from),
  • luego especificas un filtro que deje solo los que te interesen (where) y
  • finalmente le indicas una expresión de cómo quieres proyectar tus datos en la salida (select).

En este caso, se me hizo más fácil elegir primero todos los elementos <Issue> dentro del documento XML, ya que a partir de ellos puedo obtener el resto de la información viendo los nodos ancestros.  Logré hacer esto con una sola línea en el from gracias al método Descendants() de la clase XElement.  En otras palabras estoy diciendo “selecciona los elementos <Issue> en el documento, y por cada uno de ellos dame una variable e de donde extraer más información”.  Puedes pensarlo como algo parecido a un foreach.

Luego, indiqué en el filtro where que únicamente deseo los <Issue> donde el mensaje (<Message>) sea de tipo vulnerabilidad de SQL (TypeName=”ReviewSqlQueriesForSecurityVulnerabilities”).  Como los elementos <Message> son ancestros de <Issue>, utilicé Ancestors() para acceder a ese elemento y luego Attributes() para obtener el valor de atributo TypeName y poder hacer la comparación.

Finalmente, en la proyección el select new crea un tipo (una clase pues) sin nombre con 5 propiedades, que son inicializadas con los valores de las expresiones.  Esto crea una collección de tipo IEnumerable<T> donde T es el tipo anónimo—¿ves por qué fue necesaria la variable var?

El resultado del query es este (da clic para ver la imagen más grande):

resultados del primer query

Pero, ¿qué tal si quisiera ordenar mis resultados primero por Module, luego por Namespace, Type y Method sucesivamente?  Sencillo.  Puedo agregar una cadena de OrderBy’s a mi query original o puedo agarrar el resultado del query y ordenarlo así:

var ordered =
    query.OrderBy(x => x.Method).OrderBy(x => x.Type)
    .OrderBy(x => x.Namespace).OrderBy(x => x.Module);

Esta manera de hacerlo involucra las famosas expresiones lamda, pero no me quiero desviar en explicarlas.  Por ahora sólo tómalas como ejemplo y nota que puedes encadenar las cláusulas.

Esto es un buen comienzo, pero ahora quiero esos datos en algo que se le parezca a un documento XML.

Entonces agregué esto después de mi query:

XDocument xdoc = new XDocument();
xdoc.AddFirst(new XElement("Vulnerabilities"));
 
foreach (var element in query)
{
    xdoc.Root.Add(
        new XElement("Vulnerability", new XElement("Module", element.Module)
                                    , new XElement("Namespace", element.Namespace)
                                    , new XElement("Type", element.Type)
                                    , new XElement("Method", element.Method)
                                    , new XElement("Issue", element.Issue)
                                    ));
}

Primero creo un nuevo documento XML utilizando la clase XDocument, y le agrego un elemento raíz <Vulnerabilities>.  Luego por cada elemento de mi lista ordenada, agrego un elemento <Vulnerability> que contendrá cinco elementos, uno por cada propiedad que me interesó, y que tendrán el valo.  Nota que los constructores “raros” de XElement facilitan enormemente la tarea, y que los métodos AddFirst() y AddNew() se encargará de cerrar los elementos adecuadamente—a diferencia de la forma en que se utiliza un XmlWriter, por ejemplo.

El resultado:

resultados del query

Esto ya es prácticamente lo que quiero.  Sin embargo, mi código hasta ahorita son como 30 líneas—principalmente por el formato, en realidad solo he utilizado como 7 instrucciones—y me gustaría condensar el código más.  Además descubrí que el ordenamiento en realidad no lo necesito—puedo hacerlo en Excel—y que al documento XML le falta la declaración <?xml> al pricipio.

Sintetizando el código

Así que decidí cambiar la proyección (el select pues) de mi query original.  El total del código quedó así:

XElement xml = XElement.Load(@"C:\Users\Carlos\Desktop\sampleFxCopReport.xml");
 
IEnumerable<XElement> query =
    from e in xml.Descendants("Issue")
    where e.Ancestors("Message").Attributes("TypeName").First().Value == "ReviewSqlQueriesForSecurityVulnerabilities"
    select new XElement("Vulnerability",
        new XElement("Module", e.Ancestors("Module").Attributes("Name").First().Value),
        new XElement("Namespace", e.Ancestors("Namespace").Attributes("Name").First().Value),
        new XElement("Type", e.Ancestors("Type").Attributes("Name").First().Value),
        new XElement("Method", e.Ancestors("Member").Attributes("Name").First().Value),
        new XElement("Issue", e.Value)
        );
 
XDocument xDoc = new XDocument(
    new XDeclaration("1.0", "UTF-8", "yes"),
    new XElement("Vulnerabilities", query)
    );

Nota que con solo 2 instrucciones—la instrucción de carga no cuenta—logré hacer la transformación (¡!) smile_omg.   Veamos los cambios significativos al query.

Primero, el resultado es almacenado en una variable que ya no es tipo var, sino IEnumerable<T> donde T es XElement.  Esto es porque en la proyección (el select) ya no estoy utilizando un tipo anónimo para construir un objeto, sino los constructores de la clase XElement para obtener objetos de este tipo.

Finalmente, a la hora de crear el nuevo documento XML, utilizo también un constructor alterno de XDocument, para primero pasarle la declaración XML, y agregarle un elemento <Vulnerabilities> que tendrá como contenido el resultado de mi query—la lista de nodos <Vulnerability> con sus respectivos elementos hijo.  ¿Así o más mágico? smile_teeth

[Pronunciarse en tono de vendedor de chuchulucos por T.V.:] ¡Pero aún hay más!

¿Quieres saber lo que se necesita para guardar el nuevo documento de XML a un archivo?

xDoc.Save(@"C:\Users\Carlos\Desktop\transformedReport.xml");

Así que con un total de 4 instrucciones logré cargar un archivo complejo de XML, transformarlo y escribir los resultados a otro más amigable.

[Pronunciarse en tono de vendedor de chuchulucos por T.V.:] ¡Pero aún hay más!

Sintetizándo el código aún más

¿En verdad quieres ponerte quisiquilloso con el query?  Puedes lograr la totalidad de la lógica con solo 3 instrucciones—una para cargar el archivo original, otra para transformarlo y una más para guardar el archivo resultante—si utilizas expresiones lambda.  Este ejemplo no lo explico porque igual y te acabo confundiendo más, pero creo que ilustra el poderío de LINQ y las nuevas clases para manipular XML.

XElement xml = XElement.Load(@"C:\Users\Carlos\Desktop\sampleFxCopReport.xml");
 
XDocument xDoc = new XDocument(
  new XDeclaration("1.0", "UTF-8", "yes"),
    new XElement("Vulnerabilities",
      xml.Descendants("Issue")
      .Where(i => i.Ancestors("Message").Attributes("TypeName").First().Value == "ReviewSqlQueriesForSecurityVulnerabilities")
      .Select(e => new XElement("Vulnerability",
              new XElement("Module", e.Ancestors("Module").Attributes("Name").First().Value),
              new XElement("Namespace", e.Ancestors("Namespace").Attributes("Name").First().Value),
              new XElement("Type", e.Ancestors("Type").Attributes("Name").First().Value),
              new XElement("Method", e.Ancestors("Member").Attributes("Name").First().Value),
              new XElement("Issue", e.Value)
              )
          )
      )
  );
 
xDoc.Save(@"c:\users\Carlos\Desktop\transformedReport.xml");

¿Qué tal, eh? smile_nerd

Conclusión

Este artículo mostró un ejemplo de cómo transformar un documento XML utilizando LINQ to XML.  También mostró de manera práctica qué son los tipos anónimos (“clases sin nombres”), cómo cargar y escribir archivos XML con las clases XElement y XDocument incluidas en el .NET Framework 3.5, e ilustró brevemente el uso de expresiones lambda.  La mayoría de los conceptos—excepto quizá las expresiones lambda—aplican también si estás utilizando Visual Basic como tu lenguaje de desarrollo para .NET.

Enjoy. smile_shades