Supongamos que tenemos una clase Message, que representa un mensaje tipo SMS que se pueda enviar de un teléfono a otro. No parece complicado; necesitamos dos números (el remitente y el receptor), y el mensaje en sí como texto, que no puede ser la cadena vacía.
public class Message {
public Message(int from, int to, string msg)
{
Debug.Assert( !string.IsNullOrEmpty( msg ) );
this.From = from;
this.To = to;
this.Msg = msg;
}
public int From { get; }
public int To { get; }
public string Msg { get; }
public override string ToString()
{
return $"From: {From} -> To: {To}: \"{Msg}\"";
}
}
De acuerdo, no ha sido difícil. Como el mensaje ya no se modifica una vez se va a enviar, las propiedades son de solo lectura. Además, el constructor comprueba que realmente hay un mensaje para construir un objeto Message válido.
JSON
Serializar este objeto a JSON es incluso hasta demasiado fácil: solo tenemos que incluir un código como el siguiente:
var m1 = new Message( 455123132, 555789789, "¿Peli y manta esta noche?" );
File.WriteAllText( "msg.json", JsonSerializer.Serialize( m1 ) );
Hagámoslo "oficial":
public class JsonMessage {
public JsonMessage(Message m)
{
this.Message = m;
}
public void Save(string fn)
{
File.WriteAllText( fn, JsonSerializer.Serialize( this.Message ) );
}
public Message Message { get; }
}
Podemos invocarlo con:
new JsonMessage(
new Message( 455123132, 555789789, "¿Peli y manta esta noche?" ) )
.Save( "msg.json" );
Y se genera el archivo:
{"From":455123132,"To":555789789,"Msg":"\u00BFPeli y manta esta noche?"}
Por ahora, todo bien. Siempre y cuando este formato nos valga, y que no queramos hacer ningún preprocesado o algo parecido.
Pero en la clase JSONMessage, falta algo. Deberíamos pode recuperar mensajes... Para ello, debería ser tan sencillo como hemos escrito el código hasta ahora. Y efectivamente, lo es.
class JsonMessage {
// más cosas...
public static Message Load(string fn)
{
return JsonSerializer.Deserialize<Message>(
File.ReadAllText( fn )
?? throw new IOException( "error reading file: " + fn ) )
?? throw new JsonException( "error reading JSON from: " + fn );
}
}
El operador ?? funciona evaluando la expresión a su izquierda. Si no devuelve null, entonces devuelva esa misma expresión. Si devuelve null, entonces devuelve la expresión a su derecha. Un ejemplo rápido sería: null ?? 5
, que devolvería 5. En el código de ahí arriba, se utiliza para lanzar una excepción si se produce algún error, bien sea leyendo el archivo completo (eso es lo que hace File.ReadAllText()), o bien interpretando el código JSON de su interior.
Podemos recuperar entonces el mensaje con el código siguiente, en este caso mostrándolo por pantalla.
Console.WriteLine( JsonMessage.Load( "msg.json" ) );
JSON es una propuesta relativamente reciente, y su implementación es tan buena que no le importa que ni siquiera las propiedades sean de solo lectura (¿te habías dado cuenta?), ya que por reflexión ("internamente", sin respetar el interfaz de la clase), hace el enlace entre las propiedades y los datos leídos sin problema.
XML
Si deseamos serializar con XML, parece que vamos a poder hacer algo muy parecido, así que nos ponemos manos a la obra:
public class XmlMessage {
public XmlMessage(Message m)
{
this.Message = m;
}
public Message Message { get; }
public void Save(string fn)
{
using ( var f = File.OpenWrite( fn ) ) {
new XmlSerializer( this.Message.GetType() )
.Serialize( f, this.Message );
}
}
public static Message Load(string fn)
{
using ( var f = File.OpenRead( fn ) ) {
return (Message) new XmlSerializer( typeof( Message ) )
.Deserialize( f )
?? throw new XmlException( "error reading XML from: " + fn );
}
}
}
Es un código muy parecido al que hemos creado para JSON, aunque ahora vamos a manejar XML. Si lo ejecutamos...
Unhandled exception. System.InvalidOperationException: Message cannot be serialized because it does not have a parameterless constructor.
at System.Xml.Serialization.TypeDesc.CheckSupported()
...
at XmlMessage.Save(String fn) in Program.cs:line 40
Qué curioso... Resulta que cuando intentamos crear el serializador en XmlMessage.Save()... este comprueba si la clase es soportada. Para que sea soportada, tiene que tener un constructor sin parámetros.
A partir de aquí tenemos un par de opciones. La primera es sentirse acosado por el IDE, que seguramente mostrará un subrayado rojo en el que nos dice que creemos un constructor sin parámetros en la clase Message. Probablemente sintamos una cierta ansiedad por pulsar en la bombilla, y hacer que el subrayado desaparezca.
Claro que también podemos mantener la cabeza fría, y pararnos a pensar si eso tiene sentido. En nuestro ejemplo, la clase Message utiliza el constructor para garantizar que el texto del mensaje no está vacío, así como que se pasan dos números, el del remitente y el del receptor. Si creamos un constructor sin parámetros, todas esas invariantes desaparecerán.
Una opción es construir nosotros mismos el XML. Sí, sí, no duele nada, prometido.
public XmlMessage2 {
// más cosas...
public void Save(string fn)
{
new XElement("Message",
new XAttribute( "from", this.Message.From ),
new XAttribute( "to", this.Message.To ),
this.Message.Text ).Save( fn );
}
public static Message(string fn)
{
var msgElem = XElement.Load( fn ) ?? throw new IOException( "reading " + fn );
int from = (int?) msgElem.Attribute( "from" ) ?? throw new XmlException( "missing from" );
int to = (int?) msgElem.Attribute( "to" ) ?? throw new XmlException( "missing to" );
return new Message( from, to, msgElem.Value );
}
}
Es un código hecho por nosotros, y por tanto a la medida del problema. Por ejemplo, si es necesario ignorar una propiedad, o grabar un valor calculado, no hay problema, es trivial. En contrapartida, tiene el "problema" de que no cambia cuando la clase cambia, sino que tenemos que mantenerlo aparte.
El resultado, por otra parte, es el esperado.
<?xml version="1.0" encoding="utf-8"?>
<Message from="455123132" to="555789789">
¿Peli y manta esta noche?
</Message>
La segunda y última posibilidad es crear un objeto DTO (Data Transfer Object, u Objeto de Transferencia de Datos). Es decir, creamos una clase MessageDTO que no tiene en cuenta las invariantes de la clase, para después leer los datos del objeto de la misma. Así, no es necesario comprometer las invariantes de la clase Message. Tenemos que tener la posibilidad de convertir entre objetos de ambas clases sin problema.
public class MessageDTO {
public int From { get; set; }
public int To { get; set; }
public string Msg { get; set; }
public override string ToString()
{
return $"[MessageDTO [From: {From}][To: {To}]: \"{Msg}\"]";
}
public Message ToMessage()
{
return new Message( this.From, this.To, this.Msg );
}
public static MessageDTO FromMessage(Message m)
{
return new MessageDTO { From = m.From, To = m.To, Msg = m.Msg };
}
}
...y ahora ya podemos utilizar la primera variante del XML, pero a través de MessageDTO. El programador usuario de la clase no tiene por qué saber que la estamos utilizando, solo debe conocer Message, que es lo que se acepta y lo que se devuelve. Eso sí, debe ser una clase pública, lo que es un requisito del serializador de XML.
public class XmlMessage3 {
public XmlMessage3(Message m)
{
this.Message = m;
}
public Message Message { get; }
public void Save(string fn)
{
using ( var f = File.OpenWrite( fn ) ) {
new XmlSerializer( typeof( MessageDTO ) )
.Serialize(
f, MessageDTO.FromMessage( this.Message ) );
}
}
public static Message Load(string fn)
{
using ( var f = File.OpenRead( fn ) ) {
return ( (MessageDTO) new XmlSerializer( typeof( MessageDTO ) )
.Deserialize( f ) ).ToMessage()
?? throw new XmlException( "error reading XML from: " + fn );
}
}
}
El XML generado ahora cambia ligeramente.
<?xml version="1.0" encoding="utf-8"?>
<MessageDTO
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<From>455123132</From>
<To>555789789</To>
<Msg>¿Peli y manta esta noche?</Msg>
</MessageDTO>
¿Y tú? ¿Construyes tu propio XML, o una clase DTO cuando es necesario, o te dejas llevar por la "fiebre del subrayado rojo" ;-) ?
Top comments (0)