Lo que todo desarrollador debería saber sobre serialización a XML en .NET (y cómo afecta a los Web Services)
lunes 23 de abril de 2007 | categorías: .net, c#, los mejorcitos, web services, xml | 6 comentarios -- da clic aquí para dejar el tuyoEn la plática y taller de este mes, alguien hizo una pregunta muy común: "Quiero construir un Web Service en .NET que haga X, ¿por dónde comienzo?"
Así que le prometí escribir un artículo al respecto. Sin embargo, en cuanto comencé, me di cuenta que hay un concepto más básico que debe ser amaestrado para en verdad entender lo que está sucediendo: Serialización. Específicamente serialización a XML.
¿Qué es serialización y cómo se utiliza?
Serialización, no es más que una palabra dominguera que significa transformar una instancia de una clase a una serie de bytes con un formato determinado. En este caso estoy hablando de agarrar una instancia de una clase y transformarla a un documento XML.
En la plática, algunas personas se sorprendieron cuando les dije que prácticamente cualquier clase podía ser serializada casi automáticamente, de una manera relativamente sencilla. Solo hay un "pero" del que debes estar consciente: únicamente los miembros y propiedades públicas pueden ser serializados. En otras palabras, no convierte métodos, indexadores, campos privados o protegidos o propiedades solo-lectura (excepto colecciones solo-lectura).
Toma como ejemplo una clase sencilla como la siguiente:
using System;public class MiClase
{protected string _campo1 = "campo1 es protegido";
public string _campo2 = "campo2 es publico";
private string _campo3 = "campo3 es privado";
private int _propiedad1 = 1000;
private int _propiedad2 = 9999;
public int _propiedad3 = 7777;
public int UnaPropiedad
{get { return _propiedad1; }
set { _propiedad1 = value; }
}
protected int OtraPropiedad
{get { return _propiedad2; }
set { _propiedad2 = value; }
}
public int PropiedadPublica
{get { return _propiedad3; }
}
// Se requiere un constructor publico default para que // funcione la serializacion public MiClase() { }}
Podemos serializarla (transformarla a XML pues) con un código como este:
using System;using System.IO;using System.Xml.Serialization;class Program
{static void Main(string[] args)
{MiClase m = new MiClase();
XmlSerializer serializador = new XmlSerializer(typeof(MiClase));
StringWriter escritor = new StringWriter();
// se puede serializar a casi cualquier tipo de Stream o Writer // p.ej. System.IO.StreamWriter, System.Xml.XmlWriter, etc.serializador.Serialize(escritor, m);
Console.WriteLine(escritor.ToString());}
}
Si corremos el programa, el resultado será este:

O, visto de una manera más amigable (utilizando el visualizador de XML de Visual Studio):

Nota que únicamente el _campo2, _propiedad3 y UnaPropiadad fueron serializados, ya que eran los únicos con un nivel de acceso public. PropiedadPublica, a pesar de ser public, era solo-lectura y por lo tanto no fue serializada. También nota que automáticamente construyó el documento utilizando el nombre de la clase (MiClase) como el elemento raíz y tomó el nombre de cada campo para los elementos hijos (_campo2, _propiedad3, UnaPropiedad).
La magia la hace la clase XmlSerializer, la cual tiene 3 métodos interesantes:
- Serialize() convierte una instancia de una clase a XML.
- Deserialize() hace lo contrario, toma un documento XML y lo transforma en una instancia de una clase.
- CanDeserialize() regresa un booleano para probar si el XML en realidad se puede des-serializar.
Ahora, puedes controlar varios aspectos de la serialización, aplicando atributos:
- [Serializable] no solo indica que la clase puede ser serializada, sino que también revisa que todos los tipos contenidos dentro de la clase (otra clase, por ejemplo) también sean serializables. Si no lo son, entonces arroja un SerializationException.
- [NonSerialized] y [XmlIgnore] indican que no queremos que se serialize el campo o propiedad. El primero afecta al SoapFormatter (el utilizado por WebServices), y el segundo afecta a XmlSerializer.
- [XmlRoot], que su vez tiene parámetros como Namespace y ElementName para especificar el nombre y espacio de nombres del elemento raíz. Este se aplica solo a la clase.
- [XmlElement], es parecido al anterior, pero se aplica a los campos o propiedades. Puedes especificar el tipo de dato de XML Schema que debe aplicar y si es o no nulleable mediante los parámetros DataType y IsNullable. También puedes especificar el espacio de nombres y el nombre del elemento como en XmlRoot.
- [XmlAttribute] indica que quieres serializar el valor como un atributo, en lugar de un elemento.
Para ver el impacto, vamos a aplicar estos conceptos al ejemplo anterior:
using System;using System.Xml.Serialization;[Serializable()] //indica explicitamente que esta clase es serializable
[XmlRoot(Namespace = "http://comunidadnetjuarez.org/2007/04",
ElementName = "MiClaseSerializada")] // indica el namespace y nombre
public class MiClase // del elemento raiz
{protected string _campo1 = "campo1 es protegido";
[XmlElement(ElementName = "SegundoCampo",
DataType = "string",IsNullable = false)] // indica el nombre y tipo del elemento
public string _campo2 = "campo2 es publico";
private string _campo3 = "campo3 es privado";
private int _propiedad1 = 1000;
private int _propiedad2 = 9999;
[NonSerialized()] // no serializar cuando se use SoapFormatter
[XmlIgnore()] // no serializar cuando se use XmlSerializer
public int _propiedad3 = 7777;
[XmlAttribute(AttributeName = "valor")] // indica que deseamos que
public int UnaPropiedad // se serialize como atributo XML
{ // y no como elemento XMLget { return _propiedad1; }
set { _propiedad1 = value; }
}
protected int OtraPropiedad
{get { return _propiedad2; }
set { _propiedad2 = value; }
}
public int PropiedadPublica
{get { return _propiedad3; }
}
// Se requiere un constructor publico default para que // funcione la serializacion public MiClase() { }}
El resultado sería el siguiente:

La cosa comienza a ponerse más interesante mientras más complejidad le agreguemos a la clase. Por ejemplo, ¿qué pasa si mi clase tiene una colección o lista de cosas? Bueno, pues resulta que únicamente las colecciones que implementen ICollection o IEnumerable podrán serializarse (p.ej. un ArrayList). Si implementan IDictionary (como un HashTable) estas no son serializadas (de hecho levanta una excepción).
Agreguemos el siguiente miembro a la clase:
public string[] arreglo = { "arreglo uno",
"arreglo dos", "arreglo tres" };El resultado sería el siguiente:

Y finalmente veamos qué pasa si aplicamos [XmlElement]:
[XmlElement(ElementName = "UnArreglo")]
public string[] arreglo = { "arreglo uno",
"arreglo dos", "arreglo tres" };
Creación automática de una clase serializable
Como puedes ver, tienes bastante control sobre la conversión de una clase .NET a un documento XML. Sin embargo, mientras más complejo sea el documento XML que quieras producir, más complejo y cuidadoso tendrás que ser con la aplicación de los atributos para obtener el resultado que quieres.
Muchas veces quizá es más sencillo comenzar con un documento XML de ejemplo o mejor aún con el contrato XML Schema que debe cumplir el documento XML que quieres producir cuando se serialice tu clase. Una vez que tienes esto, puedes utilizar la utilería xsd.exe para automáticamente generar los tipos adecuados en C# (o VB.NET).
Veamos un ejemplo. Supongamos que tienes un archivo llamado SchemaEjemplo.xsd con el siguiente contenido:
<?xml version="1.0" encoding="utf-8" ?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="personas">
<xs:complexType>
<xs:sequence>
<xs:element name="persona"
type="personaType"
minOccurs="1"
maxOccurs="unbounded" />
</xs:sequence>
</xs:complexType>
</xs:element><xs:complexType name="personaType">
<xs:sequence>
<xs:element name="titulo"
type="tituloType"
minOccurs="0" />
<xs:element name="nombre"
type="nombreType" />
<xs:element name="edad"
type="xs:integer" />
<xs:element name="direccion"
type="direccionType" />
<xs:element name="genero"
type="generoType" />
</xs:sequence>
<xs:attribute name="colorOjos"
type="xs:string" />
</xs:complexType>
<xs:simpleType name="tituloType">
<xs:restriction base="xs:string">
<xs:enumeration value="Sr." />
<xs:enumeration value="Sra." />
<xs:enumeration value="Señorita" />
</xs:restriction>
</xs:simpleType>
<xs:complexType name="direccionType">
<xs:sequence>
<xs:element name="calle"
type="xs:string" />
<xs:element name="numero"
type="xs:string" />
<xs:element name="numeroDepartamento"
type="xs:string"
minOccurs="0" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="nombreType">
<xs:all>
<xs:element name="primerNombre"
type="xs:string" />
<xs:element name="segundoNombre"
type="xs:string"
minOccurs="0" />
<xs:element name="apellidos"
type="xs:string" />
</xs:all>
</xs:complexType>
<xs:complexType name="generoType">
<xs:choice>
<xs:element name="hombre"
type="xs:boolean" />
<xs:element name="mujer"
type="xs:boolean" />
</xs:choice>
</xs:complexType>
</xs:schema>
La siguiente línea de comando de Visual Studio 2005 genera las clases necesarias:
xsd.exe SchemaEjemplo.xsd /classes /language:CS
El archivo resultante (SchemaEjemplo.cs) se ve algo así como este (solo muestro una parte dado que es muy largo el fragmento de código). Nota que define una clase fuertemente tipada por cada elemento o complexType definido en el Schema:
//------------------------------------------------------------------------------// // This code was generated by a tool.// Runtime Version:2.0.50727.42//// Changes to this file may cause incorrect behavior and will be lost if// the code is regenerated.// //------------------------------------------------------------------------------using System.Xml.Serialization;// // This source code was auto-generated by xsd, Version=2.0.50727.42.// ///
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "2.0.50727.42")]
[System.SerializableAttribute()][System.Diagnostics.DebuggerStepThroughAttribute()][System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)]
[System.Xml.Serialization.XmlRootAttribute(Namespace = "", IsNullable = false)]
public partial class personas
{private personaType[] personaField;
///
[System.Xml.Serialization.XmlElementAttribute("persona",
Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]public personaType[] persona
{get { return this.personaField; }
set { this.personaField = value; }
}
}
///
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "2.0.50727.42")]
[System.SerializableAttribute()][System.Diagnostics.DebuggerStepThroughAttribute()][System.ComponentModel.DesignerCategoryAttribute("code")]
public partial class personaType
{private tituloType tituloField;
private bool tituloFieldSpecified;
private nombreType nombreField;
private string edadField;
private direccionType direccionField;
private generoType generoField;
private string colorOjosField;
///
[System.Xml.Serialization.XmlElementAttribute( Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]public tituloType titulo
{get { return this.tituloField; }
set { this.tituloField = value; }
}
///
[System.Xml.Serialization.XmlIgnoreAttribute()]public bool tituloSpecified
{get { return this.tituloFieldSpecified; }
set { this.tituloFieldSpecified = value; }
}
///
[System.Xml.Serialization.XmlElementAttribute( Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]public nombreType nombre
{get { return this.nombreField; }
set { this.nombreField = value; }
}
///
[System.Xml.Serialization.XmlElementAttribute(Form = System.Xml.Schema.XmlSchemaForm.Unqualified, DataType = "integer")]
public string edad
{get { return this.edadField; }
set { this.edadField = value; }
}
///
[System.Xml.Serialization.XmlElementAttribute( Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]public direccionType direccion
{get { return this.direccionField; }
set { this.direccionField = value; }
}
///
[System.Xml.Serialization.XmlElementAttribute( Form = System.Xml.Schema.XmlSchemaForm.Unqualified)]public generoType genero
{get { return this.generoField; }
set { this.generoField = value; }
}
///
[System.Xml.Serialization.XmlAttributeAttribute()]public string colorOjos
{get { return this.colorOjosField; }
set { this.colorOjosField = value; }
}
}
// ... así continúa ...Una vez que tus clases han sido generadas es solo cuestión de utilizarlas:
using System; using System.IO; using System.Xml.Serialization; using System.Xml; class Program
{ static void Main(string[] args)
{ // objeto principal personas p = new personas();
// declara dos personas personaType p1 = new personaType();
personaType p2 = new personaType();
// poner las propiedades de p1 p1.colorOjos = "Cafe"; direccionType d1 = new direccionType();
d1.calle = "Chinches Bravas"; d1.numero = "123"; d1.numeroDepartamento = ""; p1.direccion = d1;
p1.edad = "25"; generoType g1 = new generoType();
g1.ItemElementName = ItemChoiceType.hombre; g1.Item = true; p1.genero = g1;
nombreType n1 = new nombreType();
n1.primerNombre = "Chucho"; n1.segundoNombre = "El"; n1.apellidos = "Roto"; p1.nombre = n1;
p1.titulo = tituloType.Sr; // poner las propiedades de p2 p2.colorOjos = "Azul"; direccionType d2 = new direccionType();
d2.calle = "La laguna"; d2.numero = "7777"; d2.numeroDepartamento = "9A"; p2.direccion = d2;
p2.edad = "15"; generoType g2 = new generoType();
g2.ItemElementName = ItemChoiceType.mujer; g2.Item = true; p2.genero = g2;
nombreType n2 = new nombreType();
n2.primerNombre = "Lola"; n2.segundoNombre = "La"; n2.apellidos = "Trailera"; p2.nombre = n2;
p2.titulo = tituloType.Señorita; // agregar p1 y p2 al array de personas p.persona = new personaType[] {p1, p2};
// // serializar la instancia // XmlSerializer serializador = new XmlSerializer(typeof(personas));
StringWriter escritor = new StringWriter();
serializador.Serialize(escritor, p);
Console.WriteLine(escritor.ToString()); }
}
El resultado:

Bueno, ¿pero qué carajos tiene que ver con Web Services?
Pues sin ahondar mucho en detalles (eso lo haré en otro artículo), resulta que uno de los primeros pasos al crear un Web Service es precisamente definir el formato de los mensajes que se intercambiarán entre el cliente y el servicio (despues de todo, Web Services, visto desde un punto de vista sobre-simplificado no son más que mensajes XML sobre HTTP). ASP.NET y el .NET Framework automáticamente hacen este tipo de conversión de y hacia XML.

También es importante comprender la serialización, porque la siguiente generación de herramientas para servicios (Windows Communication Foundation) utiliza otros mecanismos para transformar tipos de .NET a XML y hay algunas diferencias importantes. Así que solo hemos comenzado con el tema, pero por ahora creo que es suficiente.


