Cosa è la Serializzazione ?
Serializzare un oggetto significa salvare il suo stato in un dato momento, tipicamente per trasmetterlo ad un altro strato software o per salvarlo su disco e ricaricarlo successivamente. Il .NET Framework ci offre almeno tre possibilità per serializzare gli oggetti: BinaryFormatter, SoapFormatter ed XmlSerializer. In questo articolo cercheremo di capire quando abbia senso utilizzare una o l’altra tecnica, con particolare attenzione alla serializzazione Xml, quindi a SoapFormatter e soprattutto ad XmlSerializer, che risultano fondamentali nello sviluppo dei Web Services.
[Immagine 01: Schema delle serializzazioni]
BinaryFormatter
La classe BinaryFormatter (System.Runtime.Serialization.Formatters.Binary) permette di serializzare e deserializzare oggetti e strutture di oggetti in formato binario. È pensata per persistere in formato binario lo stato di un oggetto, comprendendo in questo le proprietà pubbliche e private con i loro tipi e le metainformazione sulla classe. Dal momento che è anche possibile serializzare strutture di oggetti con riferimenti incrociati o circolari, il BinaryFormatter si fa carico di non serializzare inutilmente e più volte oggetti già serializzati, mantiene quindi traccia dei riferimenti ed ottimizza il consumo di risorse. Unica condizione affinchè gli oggetti possano essere serializzati correttamente è che tutte le classi su cui si basano presentino l’attributo Serializable. Proviamo a vedere un esempio di codice che serializza un oggetto con il BinaryFormatter:
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
namespace BinaryTest01
{
[Serializable]
class clsPersona
{
public String nome;
public Int32 eta;
private String id = Guid.NewGuid().ToString();
}
class clsMain
{
static void Main(string[] args)
{
clsPersona objPersona = new clsPersona();
objPersona.nome = "Mario Rossi";
objPersona.eta = 32;
Stream objStreamOut = new FileStream("persona.bin",
FileMode.Create, FileAccess.Write, FileShare.None);
BinaryFormatter objBinFormatter = new BinaryFormatter();
objBinFormatter.Serialize(objStreamOut, objPersona);
objStreamOut.Close();
}
}
}
Come si vede è sufficiente istanziare un oggetto BinaryFormatter, preparare un oggetto Stream per contenere il risultato della serializzazione e quindi richiamare il metodo Serialize del BinaryFormatter, passando come argomenti lo Stream di output e l’oggetto da serializzare.
Non possiamo in questo caso valutare agevolmente il contenuto del file binario prodotto, possiamo però provare a ricaricarlo in un oggetto (deserializzarlo) per verificare che l’operazione sia andata a buon fine.
Per utilizzare nuovamente l’oggetto sarà quindi sufficiente deserializzarlo, cioè appoggiandosi al metodo Deserialize dell’oggetto BinaryFormatter, riottenerlo nel suo stato originale:
clsPersona objPersona;
Stream objStreamIn = new FileStream("persona.bin",
FileMode.Open, FileAccess.Read, FileShare.None);
BinaryFormatter objBinFormatter = new BinaryFormatter();
objPersona = (clsPersona)objBinFormatter.Deserialize(objStreamIn);
objStreamOut.Close();
È importante sottolineare che durante il processo di serializzazione vengono salvati nel file anche i valori delle proprietà private dell’oggetto, inoltre alla deserializzazione non viene ricostruito l’oggetto da zero, infatti non viene richiamato alcun costruttore, ma viene solo “reidratato” l’oggetto. È infine importante operare il casting corretto dell’oggetto nel deserializzarlo, in quanto la serializzazione contiene le metainformazioni sul tipo originario e otterremmo un errore di casting se cercassimo di deserializzare l’oggetto su un altro tipo.
SoapFormatter
Il SoapFormatter (System.Runtime.Serialization.Formatters.Soap) permette di serializzare oggetti con le stesse funzionalità del BinaryFormatter, la fondamentale differenza, non da poco, è che il risultato della serializzazione non è un file binario, bensì un documento XML costruito secondo la grammatica della specifica SOAP (
http://www.w3.org/2001/06/soap-envelope).
Valutiamo subito il codice necessario per serializzare un oggetto con il SoapFormatter:
Stream objStreamOut = new FileStream("persona.soap.xml",
FileMode.Create, FileAccess.Write, FileShare.None);
SoapFormatter objSoapFormatter = new SoapFormatter();
objSoapFormatter.Serialize(objStreamOut, objPersona);
objStreamOut.Close();
Come si vede la sintassi è identica a quella del BinaryFormatter, e questo non deve stupirci visto che entrambe implementano l’interfaccia IFormatter (System.Runtime.Serialization).
Anche la deserializzazione è assolutamente il parallelo della precedente:
Stream objStreamIn = new FileStream("persona.soap.xml",
FileMode.Open, FileAccess.Read, FileShare.None);
SoapFormatter objSoapFormatter = new SoapFormatter();
objPersona = (clsPersona)objSoapFormatter.Deserialize(objStreamIn);
objStreamIn.Close();
La differenza sostanziale rispetto al BinaryFormatter è proprio che avremo un documento SOAP come il seguente, nel caso per esempio di un oggetto di tipo clsPersona:
<SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:clr="http://schemas.microsoft.com/soap/encoding/clr/1.0" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<a1:clsPersona id="ref-1" xmlns:a1="http://schemas.microsoft.com/clr/nsassem/SoapTest01/SoapTest01%2C%20Version%3D1.0.768.22433%2C%20Culture%3Dneutral%2C%20PublicKeyToken%3Dnull">
<nome id="ref-3">Mario Rossi</nome>
<eta>32</eta>
<id id="ref-4">a7133f84-fa37-4ca8-8ebe-ec1f33179b86</id>
</a1:clsPersona>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
In questo secondo esempio si può esplicitamente notare che anche il campo privato id è stato serializzato nel documento SOAP. Anche in questo caso alla deserializzazione non verrà richiamato il costruttore dell’oggetto.
XmlSerializer
La classe XmlSerializer (System.Xml.Serialization) fornisce come nei due casi precedenti la possibilità di serializzare e deserializzare oggetti. Gli oggetti saranno salvati in formato XML e presenteranno solo le proprietà pubbliche e non quelle private, inoltre non saranno presenti informazioni sull’identità e sull’assembly della classe su cui si basa l’oggetto. Vediamo subito un esempio di codice e facciamo poi qualche considerazione:
public class clsPersona
{
public String nome;
public Int32 eta;
private String id = Guid.NewGuid().ToString();
}
[...]
Stream objStreamOut = new FileStream("persona.xml",
FileMode.Create, FileAccess.Write, FileShare.None);
XmlSerializer objXmlFormatter = new XmlSerializer(typeof(clsPersona));
objXmlFormatter.Serialize(objStreamOut, objPersona);
objStreamOut.Close();
In questo estratto possiamo vedere che la serializzazione XML necessita a priori del tipo di oggetto che andremo a serializzare. Questo può essere fornito in diversi modi. I principali sono: come tipo (typeof(clsPersona)); tramite un oggetto XmlTypeMapping cioè un'istanza di una classe che riproduce un comportamento conforme a quanto prescritto dal W3C nel documento relativo alla specifica SOAP 1.1 (
http://www.w3.org/TR/SOAP/) nella sezione relativa alle regole di codifica dei tipi di dati. Vediamo un esempio anche di questa seconda situazione:
Stream objStreamOut = new FileStream("persona.xml",
FileMode.Create, FileAccess.Write, FileShare.None);
XmlSerializer objXmlFormatter = new XmlSerializer(new
SoapReflectionImporter(“http://www.devleap.com/nspersona” ).ImportTypeMapping(typeof(clsPersona)));
objXmlFormatter.Serialize(objStreamOut, objPersona);
objStreamOut.Close();
In pratica, come comportamento predefinito, utilizzando la prima soluzione avremo un documento XML come il seguente:
<?xml version="1.0"?>
<clsPersona xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<nome>Mario Rossi</nome>
<eta>32</eta>
</clsPersona>
mentre nel caso in cui si utilizzi un XmlTypeMapping avremo:
<?xml version="1.0"?>
<q1:clsPersona xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="id1" xmlns:q1="http://www.devleap.com/nspersona">
<nome xsi:type="xsd:string">Mario Rossi
</nome>
<eta xsi:type="xsd:int">32
</eta>
</q1:clsPersona>
Possiamo chiaramente vedere che la seconda versione di documento presenta i tipi di dati XSD esplicitati, mentre la prima soluzione rappresenta solo dei valori, all’interno di nodi di tipo testo. Inoltre nella prima soluzione non abbiamo un Namespace predefinito per il documento, mentre nel secondo caso abbiamo la possibilità di indicare un Namespace nella costruzione dell’oggetto XmlTypeMapping. Inoltre tramite altre versioni in overload del costruttore di XmlSerializer possiamo comunque indicare il Namespace di default per l'output XML da generare. E' inoltre possibile personalizzare il comportamento di un oggetto XmlSerializer tramite attributi, ne parleremo nel prossimo articolo, a prescindere dal metodo che si utilizza per passare il tipo da serializzare. Prima di occuparci di questo però vediamo come realizzare la deserializzazione XML:
Stream objStreamIn = new FileStream("persona.xml",
FileMode.Open, FileAccess.Read, FileShare.None);
XmlSerializer objXmlFormatter = new XmlSerializer(typeof(clsPersona));
objPersona = (clsPersona)objXmlFormatter.Deserialize(objStreamIn);
objStreamIn.Close();
È importante sottolineare che nel caso della deserializzazione XML gli oggetti è opportuno che presentino un costruttore predefinito e nel caso in cui lo abbiano, questo sarà richiamato alla reidratazione dell’oggetto.
Considerazioni generali
La serializzazione XML è utilizzata nei Web Service per rappresentare in messaggi SOAP i parametri in ingresso ed i valori di ritorno dei metodi remoti invocati. In realtà però può risultare comodo sfruttare un oggetto XmlSerializer anche soltanto per leggere e gestire dei contenuti XML senza doversi necessariamente appoggiare alle classi XmlReader e XmlWriter (System.Xml). Infatti con questa tecnica possiamo leggere e scrivere i nostri oggetti in formato XML, a prescindere dal fatto che conosciamo le regole di XML e le classi .NET native per utilizzarne i documenti. Le serializzazioni tramite i BinaryFormatter e SoapFormatter sono invece pensate per le applicazioni di .NET Remoting . Se volessimo schematizzare le differenze tra le tre classi potremmo fare riferimento alla seguente tabella:
|
|
BinaryFormatter
|
SoapFormatter
|
XmlSerializer
|
|
Requisiti sugli oggetti
|
Attributo Serializable
|
Attributo Serializable
|
Classe Public
|
|
Cosa serializza
|
Data members pubblici e privati, informazioni sull’assembly
|
Data members pubblici e privati, informazioni sull’assembly
|
Data members pubblici, oggetti XmlElement ed XmlNode
|
|
Cosa non serializza
|
Metodi
|
Metodi
|
Metodi, data members privati, informazioni sull’assembly
|
|
Richiama il costruttore alla deserializzazione
|
No
|
No
|
Sì
|
|
Serve l’attributo [Serializable]
|
Sì
|
Sì
|
No
|
|
Ambito di utilizzo tipico
|
.NET Remoting
|
Web Services e
.NET Remoting
|
Web Services
|
Ricordiamo sempre il fatto che, quando utilizziamo XmlSerializer, il costruttore dell’oggetto viene richiamato alla reidratazione. Possiamo renderci conto di questo fatto valutando un semplice esempio che mette a confronto le tre diverse tecniche di serializzazione. Il codice sorgente completo è disponibile nel
download associato a questo articolo, vediamone alcuni particolari:
[Immagine 02: Risultato del confronto tra le tecniche di serializzazione]
[Serializable]
public class clsPersona
{
public String nome;
public String cognome;
public EnumSesso sesso;
public Int16 eta;
private String id;
private String datacreazione;
public clsPersona()
{
this.id = Guid.NewGuid().ToString();
this.datacreazione = DateTime.Now.ToString();
}
public clsPersona(String sNome, String sCognome,
EnumSesso nSesso, Int16 nEta)
{
this.nome = sNome;
this.cognome = sCognome;
this.sesso = nSesso;
this.eta = nEta;
this.id = Guid.NewGuid().ToString();
this.datacreazione = DateTime.Now.ToString();
}
Come si vede la classe clsPersona presenta due costruttori, uno predefinito ed uno che accetta in ingresso i valori delle proprietà dell’oggetto. Eseguendo una serializzazione nei tre modi visti e deserializzando poi l’oggetto, vedremo che, nel solo caso della classe XmlSerializer, il costruttore predefinito dell’oggetto, che inizializza alla data ed ora di invocazione la proprietà privata datacreazione e ricalcola l’id, verrà richiamato.