[C#] Cómo implementar la (de)serialización personalizada.


La serialización es el proceso de convertir un objeto en un stream de bytes para poder ser persistido en una base de datos, en un fichero o preservarlo en memoria. El proceso inverso, convertir el stream de bytes en un objeto, se denomina deserialización.

Existen dos tipos de serialización:

  • Binary serialization: Serializa un objeto a un formato binario. Con este tipo de serialización se puede enviar un objeto al disco, a memoria, etc…
  • XML serialization: Serializa un objeto a un formato XML en el cual únicamente se tienen en cuenta las propiedades públicas. Esta forma de serializar es muy útil para compartir información a través de servicios web empleando SOAP.

En este post no me detendré en explicar cómo (de)serializar de forma básica un objeto, si no que veremos cómo se puede controlar de forma personalizada la (de)serialización de un objeto.

¿Qué es la custom serialization?

Es el proceso de controlar la (de)serialización de un tipo dado. Esto permite controlar la compatibilidad de versiones de dicho tipo.

Se puede controlar la (de)serialización de dos formas:

Implementando el interfaz ISerializable

Implica implementar el método GetObjectData y un constructor especial que es usado cuando el objeto es deserializado. Para aclarar las cosas: el motor de ejecución de .NET llama al método GetObjectData en la serialización del objeto y llama al constructor especial de la clase en la deserialización del mismo.

Por defecto una clase, para poderse serializar, debe estar marcada con el atributo SerializableAttribute pero si no estamos implemantando el interfaz ISeriaizable todos los miembros públicos y los privados serán serializados “por detrás”, por eso se necesita crear un constructor por defecto, sobre todo si empleamos la serialización XML.

Vamos a analizar el siguiente código de una aplicación de consola:

using System;
using System.Runtime.Serialization;
using System.Security.Permissions;

namespace TestProject
{
   public class Test
   {
      public static void Main()
      {
         string fileName = "testFileForSerialization.DAT";       
         IFormatter formatter = new BinaryFormatter();
         SerializeItem(fileName, formatter); 
         DeserializeItem(fileName, formatter);
         Console.ReadLine();
      }

      public static void SerializeItem(string fileName, IFormatter formatter)
      {
         // Creamos una instancia de Person y rellenamos sus propiedades
         Person person = new Person("Sergio", "Parra Guerra", new DateTime(1978, 7, 28));
         Console.WriteLine(string.Format("Serializando objeto: {0} {1} {2} {3}", person.Name, person.Surname, person.DateOfBirth, person.Age));

         using (FileStream stream = new FileStream(fileName, FileMode.Create))
         {
            formatter.Serialize(stream, person);
         }
      }

      public static void DeserializeItem(string fileName, IFormatter formatter)
      {
         using (FileStream stream = new FileStream(fileName, FileMode.Open))
         {
            // deserializar
            Person person = (Person)formatter.Deserialize(stream);
            Console.WriteLine(string.Format("Deserializado objeto: {0} {1} {2} {3}", person.Name, person.Surname, person.DateOfBirth, person.Age));
         }
      } 
   }
 
   [Serializable]
   public class Person : ISerializable
   {
      private readonly string name;
      private readonly string surname;
      private readonly DateTime dateOfBirth;
      private readonly int age; 
 
      public Person(string name, string surname, DateTime dateOfBirth)
      {
         this.name = name;
         this.surname = surname;
         this.dateOfBirth = dateOfBirth;

         // calculamos la edad dada la fecha de nacimiento
         this.age = DateTime.Now.Year - dateOfBirth.Year;
         if (DateTime.Now < dateOfBirth.AddYears(age)) this.age--;
      }
 
      public string Name
      {
         get { return name; }
      }

      public string Surname
      {
         get { return surname; }
      }

      public DateTime DateOfBirth
      {
         get { return dateOfBirth; }
      }
      public int Age
      {
        // Se calcula
         get { return age; }
      }
 
      private Person(SerializationInfo info, StreamingContext ctxt)
      {
         this.name = info.GetString("Name");
         this.surname = info.GetString("Surname");
         this.dateOfBirth = info.GetDateTime("DateOfBirth");
 
         // calculamos la edad dada la fecha de nacimiento
         this.age = DateTime.Now.Year - dateOfBirth.Year;
         if (DateTime.Now < dateOfBirth.AddYears(age)) this.age--;
      }

      [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]
      void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
      {
         info.AddValue("Name", this.name);
         info.AddValue("Surname", this.surname);
         info.AddValue("DateOfBirth", this.dateOfBirth);
 
         // info.AddValue("Age", this.age); No se añade Age ya que es un dato calculado en la deserialización y en el constructor
      } 
   }
}

El  método GetObjectData() es llamado cuando se solicita información que se quiere serializar y los miembros se agregan como pares Nombre-Valor como vemos en info.AddValue(). Esto permite almacenar los datos que realmente queremos serializar para restaurarlos posteriormente en la deserialización. En definitiva, el programador es responsable de qué datos se almacenan en el SerializationInfo.

Como vemos en el ejemplo, tenemos una propiedad calculada llamada Age sobre la que realizaremos el cálculo de la edad en el momento de la deserialización y en el contructor de la clase.

Si ejecutamos el código anterior de la aplicación de consola podemos observar cómo serializa y deserializa el objeto Person pasando el flujo del programa tanto por el método GetObjectData() como en el constructor especial.

Respondiendo a los eventos de la serialización

Estos eventos únicamente están disponibles en la serialización empleando la clase BinaryFormatter. Para la serialización empleando SoapFormatter estamos limitados a usar el interfaz IDeserializationCallBack. Los eventos sobre los que tenemos control son cuatro y dichos eventos llaman a métodos de la clase cuando tiene lugar la serialización o deserialización:

  • OnSerializing: Este evento se lanza antes de que la serialización tenga lugar. Decora el método con el atributo OnSerializingAttribute.
  • OnSerialized: Este evento se lanza después de que la serialización tenga lugar. Decora el método con el atributo OnSerializedAttribute.
  • OnDeserializing:  Este evento se lanza antes de que la deserialización tenga lugar. Decora el método con el atributo OnDeserializingAttribute.
  • OnDeserialized:  Este evento se lanza después de que la deserialización tenga lugar y después de que se ejecute el método IDeserializationCallBack.OnDeserialization. Decora el método con el atributo OnDeserializedAttribute.

CustomSerializationEvents

Para ver este sistema en funcionamiento modificaremos el ejemplo anterior de la siguiente forma:

using System;
using System.Runtime.Serialization;
using System.Security.Permissions;

namespace TestProject
{
   public class Test
   {
      public static void Main()
      {
         string fileName = "testFileForSerialization.DAT";       
         IFormatter formatter = new BinaryFormatter();
         SerializeItem(fileName, formatter); 
         DeserializeItem(fileName, formatter);
         Console.ReadLine();
      }

      public static void SerializeItem(string fileName, IFormatter formatter)
      {
         // Creamos una instancia de Person y rellenamos sus propiedades
         Person person = new Person("Sergio", "Parra Guerra", new DateTime(1978, 7, 28));
         Console.WriteLine(string.Format("Serializando objeto: {0} {1} {2} {3}", person.Name, person.Surname, person.DateOfBirth, person.Age));

         using (FileStream stream = new FileStream(fileName, FileMode.Create))
         {
            formatter.Serialize(stream, person);
         }
      }

      public static void DeserializeItem(string fileName, IFormatter formatter)
      {
         using (FileStream stream = new FileStream(fileName, FileMode.Open))
         {
            // deserializar
            Person person = (Person)formatter.Deserialize(stream);
            Console.WriteLine(string.Format("Deserializado objeto: {0} {1} {2} {3}", person.Name, person.Surname, person.DateOfBirth, person.Age));
         }
      } 
   }
 
   [Serializable]
   public class Person : ISerializable, IDeserializationCallback 
   {
      private readonly string name;
      private readonly string surname;
      private readonly DateTime dateOfBirth;
      private readonly int age; 
 
      public Person(string name, string surname, DateTime dateOfBirth)
      {
         this.name = name;
         this.surname = surname;
         this.dateOfBirth = dateOfBirth;

         // calculamos la edad dada la fecha de nacimiento
         this.age = DateTime.Now.Year - dateOfBirth.Year;
         if (DateTime.Now < dateOfBirth.AddYears(age)) this.age--;
      }
 
      public string Name
      {
         get { return name; }
      }

      public string Surname
      {
         get { return surname; }
      }

      public DateTime DateOfBirth
      {
         get { return dateOfBirth; }
      }
      public int Age
      {
        // Se calcula
         get { return age; }
      }
 
      private Person(SerializationInfo info, StreamingContext ctxt)
      {
         this.name = info.GetString("Name");
         this.surname = info.GetString("Surname");
         this.dateOfBirth = info.GetDateTime("DateOfBirth");
 
         // calculamos la edad dada la fecha de nacimiento
         this.age = DateTime.Now.Year - dateOfBirth.Year;
         if (DateTime.Now < dateOfBirth.AddYears(age)) this.age--;
      }

      [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]
      void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
      {
         info.AddValue("Name", this.name);
         info.AddValue("Surname", this.surname);
         info.AddValue("DateOfBirth", this.dateOfBirth);
 
         // info.AddValue("Age", this.age); No se añade Age ya que es un dato calculado en la deserialización y en el constructor
      }

      [OnSerializing]
      private void OnSerializing(StreamingContext context)
      {
         Console.WriteLine("OnSerializing.");
      }

      [OnSerialized]
      private void OnSerialized(StreamingContext context)
      {
         Console.WriteLine("OnSerialized.");
      }

      [OnDeserializing]
      private void OnDeserializing(StreamingContext context)
      {
         Console.WriteLine("OnDeserializing.");
      }

      [OnDeserialized]
      private void OnDeserialized(StreamingContext context)
      {
         Console.WriteLine("OnDeserialized.");
      }

      void IDeserializationCallback.OnDeserialization(object sender)
      {
        Console.WriteLine("IDeserializationCallback.OnDeserialization.");
      } 
   }
}

Si se ejecuta el código se obtendrá la siguiente salida:

Serializando objeto: Sergio Parra Guerra 28/07/1978 0:00:00 37
OnSerializing.
Empieza la Serialization.
OnSerialized.
OnDeserializing.
Empieza la Deserialization.
OnDeserialized.
IDeserializationCallback.OnDeserialization.
Deserializado objeto: Sergio Parra Guerra 28/07/1978 0:00:00 37

Como hemos visto se puede controlar la serialización y deserialización de un objeto aplicando los conceptos vistos en este post.

Os dejo un enlace de MSDN sobre el que me he basado para la realización de este artículo:

Custom Serialization

Anuncios

Responder

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