Escribir ficheros XML mediante LINQ


Ayer veíamos la estructura de un fichero XML y cómo hacer uso del espacio de nombres System.Xml.Linq para generar un nuevo fichero XML. Anteayer, además aprendíamos de la versatilidad de LINQ, que nos permite iterar y filtrar sobre prácticamente cualquier cosa.

Hoy vamos a aunar los valores de ambos conceptos y vamos a generar (y consultar) un fichero XML a través de LINQ. Por ello es altamente aconsejable conocer previamente los conceptos explicados en los dos artículos anteriores.

Comenzaremos por el proceso de creación de un fichero XML. O más bien, de “cumplimentación”. Hace ya unos añitos (¡cómo pasa el tiempo!) explicábamos en este mismo blog la forma de utilizar la reflexión para acceder a los elementos de un objeto (métodos, propiedades, atributos…). Hace unos días he actualizado ese artículo con información sobre cómo hacer uso de LINQ para realizar la misma operación que realizamos entonces (¡ah…! los tiempos del Framework 2.0…). Sin embargo, en este artículo iremos un poco más allá y aplicaremos esa misma funcionalidad para, a partir de un objeto, conseguir generar un fichero XML que desglose los elementos de cada miembro de un objeto.

Antes de meternos en harina con los ficheros XML, conviene centrarnos en la parte de LINQ. El objetivo es el siguiente: crear un fichero XML que almacene la información de todos los miembros de la clase de un objeto. La estructura objetivo será la siguiente:

  • <Clase>
    • <ConstructorMembers>
      • <Constructor name = “nombreConstructor” value=”void nombreConstructor()” />
    • <MethodMembers>
      • <Method name = “ToString” value = “System.String ToString()” />
      • <Method name = …

Es decir, desglosaremos la clase en secciones que, a su vez, contendrán elementos pertenecientes a esa sección (ConstructorMembers contendrá elementos Constructor y así sucesivamente), que a su vez contendrán dos atributos: name y value.

A más bajo nivel, vemos que nuestro objetivo será obtener un listado con todos los posibles miembros (constructores, métodos, propiedades…) y, una vez dentro de éste elemento, añadir uno por uno los elementos que se correspondan con esa categoría.

Recorrer una enumeración (Enum) con LINQ

Los tipos de elemento que una clase puede implementar están definidos en la enumeración System.Reflection.MemberTypes, y son las siguientes:

Sin embargo, el hecho de que estos elementos se encuentren en una enumeración (tipo Enum) no nos sirve de mucho. Queremos automatizar la tarea de recorrer cada uno de estos elementos, por lo que hacerlo a mano no entra dentro de nuestros planes. Tenemos, por lo tanto, dos posibilidades:

  • Usar una sentencia LINQ para obtener una lista con los MemberTypes presentes en el objeto que queremos analizar.
  • Transformar la enumeración en una lista.

La primera opción devolverá únicamente los tipos de miembro que están presentes en el objeto analizado.

La segunda opción, sin embargo nos creará un nodo para cada uno de los tipos de miembro que pueden existir en una clase, independientemente de si en el objeto analizado existen o no. Por interés didáctico, procederemos a hacer uso del segundo método.

Hemos visto que una enumeración, por irónico que parezca, no implementa la interfaz IEnumerable. Por lo tanto, no es posible para LINQ iterar sobre sus miembros de forma directa. De todos modos, existe una pequeña triquiñuela por la cual podemos “transformar” una enumeración en un objeto IEnumerable gracias, precisamente, a LINQ.

La clase Enum (sí, Enum es, en última instancia, una clase) posee un método estático denominado GetValues que devuelve un objeto de la clase Array conteniendo todos los miembros de la enumeración del tipo que le indiquemos como parámetro. Por lo tanto, vamos manos a la obra. Obtener todos los miembros de la clase será tan sencillo como lo siguiente:


    IEnumerable<MemberTypes> consulta = from miembro in Enum.GetValues(typeof(MemberTypes))
                                        select miembro;

Parece sencillo, ¿no? Sin embargo, si probamos a compilar vemos que no podremos finalizar nuestra tarea: hay algo que el compilador no es capaz de digerir.

Lo que el compilador nos dice es “No soy capaz de hacer iterar la variable de rango ‘miembro’ a través de un objeto de tipo Array. Haz el favor e indica de forma explícita qué tipo de variable es ‘miembro’ para que pueda iterar en paz”. O lo que es lo mismo: nos pide que hagamos un casting explícito de miembro a MemberTypes. Por lo tanto, dicho y hecho. Agregamos el casting y seguimos con nuestro trabajo.


                IEnumerable<MemberTypes> consultaMiembros = from miembro in Enum.GetValues(typeof(MemberTypes)).Cast<MemberTypes>()
                                                            select miembro;

Con esto forzaremos un casting en cada elemento recorrido por la variable de rango miembro, de modo que los tipos coincidan. Si ejecutamos esta sentencia, comprobaremos que ya dispondremos de un listado enumerable de MemberTypes que nos servirán a modo de “nodos padre” de cada uno de los miembros:

LINQ y Reflection

Es hora de pasar a la segunda parte de nuestra consulta: queremos obtener los miembros de una clase a partir de una instancia de dicha clase. ¿Es eso posible? ¡Claro que sí! Vimos en otros artículos que podía realizarse mediante un bucle y un par de condiciones. Sin embargo, LINQ se gestó para evitar este tipo de operaciones. Obtener todos los miembros de una clase a partir de un objeto es tan sencillo como lo siguiente:


    IEnumerable<MemberInfo> consulta = from miembro in objeto.GetType().GetMembers()
                                       where (miembro != null)
                                       orderby miembro.MemberType ascending
                                       select miembro;

LINQ está iterando sobre todos los miembros de la clase del objeto que no sean nulos y los está ordenando de forma ascendente. El resultado, por lo tanto, será una lista de objetos de la clase MemberInfo con información sobre cada uno de los miembros de la clase. Nos vamos, por lo tanto, acercando a nuestro objetivo.

Componiendo XML con LINQ

Comenzaremos ahora la composición del fichero XML utilizando para ello las sentencias LINQ que acabamos de definir (eso sí, con un par de modificaciones). Si a estas alturas no tenemos claro el funcionamiento de las clases XDocument, XElement y XAttribute, es un buen momento para visitar el artículo anterior y refrescar cómo se utilizan estas clases para generar ficheros XML.

Como primer paso, generaremos un fichero XML que posea un único nodo que muestre el nombre de la clase. Será tan sencillo como realizar lo siguiente:


                XDocument documentoInfoClase = new XDocument(
                    new XElement(objeto.GetType().Name));

Esto generará un documento con un único nodo cuyo nombre será el nombre de la clase del objeto que se quiere analizar (asumimos que “objeto” es el objeto que queremos analizar).

El siguiente paso será añadir todas las categorías. Sin embargo, están en una lista. Utilizaremos, por lo tanto, un bucle para recorrer la lista y añadir todos los elementos a nuestro documento XML. El código completo para acceder a los miembros de la enumeración y generar el fichero XML sería el siguiente:


                IEnumerable<MemberTypes> consultaMiembros = from tipoMiembroin Enum.GetValues(typeof(MemberTypes)).Cast<MemberTypes>()
                                                            select tipoMiembro;

                XDocument documentoInfoClase = new XDocument(
                    new XElement(objeto.GetType().Name));

                foreach (MemberTypes tipo in consultaMiembros)
                {
                    XElement elemento = new XElement(tipo.ToString());
                    documentoInfoClase.Root.Add(elemento);
                }

 

El resultado sería el siguiente:

¡¡Mal!! ¡¡Muy mal!! Sí, hemos conseguido nuestro objetivo, pero estamos desaprovechando la potencia de LINQ para que realice esta tarea de forma automática, sin necesidad de bucles. ¿Qué cómo lo hacemos? Pues en lugar de crear una variable en la que alojar la consulta, embebiendo directamente la sentencia LINQ dentro del propio nodo. ¿Suena extraño? Veamos cómo podemos hacer esto, sustituyendo el fragmento anterior por lo siguiente:


                XDocument documentoInfoClase = new XDocument(
                    new XElement(objeto.GetType().Name,             // Nodo raiz
                        from tipoMiembroin Enum.GetValues(typeof(MemberTypes)).Cast<MemberTypes>()
                        select new XElement(tipoMiembro.ToString()+ "Member")     // Cada iteración devolverá un nuevo nodo
                        )
                );

Como vimos hace un par de artículos, el operador select es capaz de crear nuevos objetos. Y, ¿por qué no crear objetos de la clase XElement que vayan incrustados dentro del nodo raíz? ¡Esa es la idea! Lo que estamos haciendo es crear un documento con un nodo raíz que contendrá el nombre de la clase. A continuación, incrustamos una sentencia LINQ que recorra todos los elementos de la enumeración y devuelva como respuesta un listado de elementos XElement cuyo contenido se corresponderá con el nombre del elemento.

Ahora nos queda la segunda parte de la ecuación: añadir cada uno de los miembros en su sección correspondiente. ¿Cómo haremos esto? Pues comparando el tipo de cada miembro con el del nivel actual en el que se está creando el nodo padre.

Por lo tanto, la idea será ampliar el constructor del XElement para añadir una nueva sentencia LINQ que generará otro listado de XElements, esta vez con la información relativa al miembro de la clase. Esquemáticamente, será algo como lo siguiente:


                XDocument documentoInfoClase = new XDocument(
                    new XElement(objeto.GetType().Name,             // Nodo raiz
                        from tipoMiembro in Enum.GetValues(typeof(MemberTypes)).Cast<MemberTypes>()
                        select new XElement(tipoMiembro.ToString() + "Member", // Cada iteración devolverá un nuevo nodo
                            // [SENTENCIA LINQ QUE GENERARÁ UN LISTADO DE XElement
                            //  CUYO TIPO COINCIDA CON tipoMiembro]
                            )
                        )
                );

Como ya conocemos la sentencia, la añadimos e incluimos el filtro en la cláusula where


                XDocument documentoInfoClase = new XDocument(
                    new XElement(objeto.GetType().Name,
                    from tipoMiembro in Enum.GetValues(typeof(MemberTypes)).Cast<MemberTypes>()
                    select new XElement(tipoMiembro.ToString() + "Member",  // Nodo de categoría

                        // Listado con los nodos correspondientes a los MemberType de la clase
                        from miembro in objeto.GetType().GetMembers()
                        where ((miembro != null) && (miembro.MemberType == tipoMiembro))
                        orderby miembro.MemberType ascending
                        select new XElement(miembro.MemberType.ToString())

                        )
                    )
                );

Nuevamente, vemos cómo la cláusula select genera un elemento XElement que será incluido dentro del XElement correspondiente a la categoría, de la forma:

  • <NombreClase>
    • <CategoriaMember>
      • <Miembro1>
      • <Miembro2>
      • <MiembroN>

Sólo nos queda un pequeño detalle: añadir los atributos a los miembros. Habíamos dicho que queríamos mostrar el nombre y el valor, por lo que lo único que tendremos que hacer será modificar el constructor del XElement más anidado e insertar allí, además del nombre del tipo (que será el nombre del elemento) dos nuevos objetos de tipo XAttribute con dos parámetros: el primero corresponderá a la clave y el segundo, al valor.


                XDocument documentoInfoClase = new XDocument(
                    new XElement(objeto.GetType().Name,
                    from tipoMiembro in Enum.GetValues(typeof(MemberTypes)).Cast<MemberTypes>()
                    select new XElement(tipoMiembro.ToString() + "Member",  // Nodo de categoría

                        // Listado con los nodos correspondientes a los MemberType de la clase
                        from miembro in objeto.GetType().GetMembers()
                        where ((miembro != null) && (miembro.MemberType == tipoMiembro))
                        orderby miembro.MemberType ascending
                        select new XElement(miembro.MemberType.ToString(),
                             new XAttribute("name", miembro.Name),
                             new XAttribute("value",
                                 miembro.GetType().IsAssignableFrom(typeof(PropertyInfo)) ? ((PropertyInfo)miembro).GetValue(objeto, null) : miembro.ToString())
                             )
                        )
                    )
                );

Lo que hacemos en el segundo parámetro del atributo “value” es indicar que, si el elemento que estamos analizando es de la clase PropertyInfo (es decir, se trata de una propiedad), almacene el valor de la misma. En caso contrario, almacenaremos la declaración del miembro en sí.

Hecho esto, veremos que el resultado es bastante satisfactorio, ajustándose a nuestras pretensiones iniciales:

Recorrido de un fichero XML

Recorrer un fichero XML es bastante similar a hacerlo sobre cualquier otro tipo de objeto. Podremos iterar sobre cualquiera de los elementos del fichero, como descendientes de un nodo o sus atributos, así como de poder hacer uso de filtros y agrupaciones para seleccionar únicamente aquellos que nos interesen.

Un ejemplo de cómo realizar esto se muestra a continuación:

            // Iteramos sobre los elementos descendientes de Juego/MethodMember y recuperamos aquellos
            // cuyo nombre sea "Method". Finalmente, calculamos la cuenta.
            int numMetodos = (from metodo in docXML.Elements("Juego").Elements("MethodMember").Descendants()
                              where metodo.Name == "Method"
                              select metodo).Count();

            // Iteramos sobre TODOS los descendientes del documento.
            // En él, seleccionamos todos aquellos nodos cuyo nombre sea "Property"
            // Sobre ese nodo, creamos un listado con todos sus atributos (nombre y valor)
            var atributosPropiedades = (from nodo in docXML.Descendants()
                                        where nodo.Name == "Property"
                                        select new {
                                            NombreNodo = nodo.Name,
                                            Atributos = (from atributo in nodo.Attributes()
                                                  select new {
                                                     Nombre = atributo.Name,
                                                     Valor = atributo.Value
                                                 })
                                            }
                                         );
            
            Console.WriteLine(string.Format("La clase posee un total de {0} metodos.", numMetodos));

            foreach (var nodo in atributosPropiedades)
            {
                Console.WriteLine(string.Format("\nELEMENTO: {0}", nodo.NombreNodo));
                foreach(var atributo in nodo.Atributos)
                {
                    Console.WriteLine(string.Format("\tNombre: {0}\tValor: {1}",
                        atributo.Nombre, atributo.Valor));
                }
            }

A lo largo de los siguientes días publicaré una serie de artículos con los que me centraré exclusivamente en la sintaxis de LINQ. En ellos definiremos los aspectos básicos de LINQ, así como de los elementos de los que éste hace uso, como tipos anónimos, delegados o expresiones lambda.

Deja un comentario

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s