In questo articolo illustro le basi della serializzazione in C Sharp
La serializzazione è quel processo per cui si vuole che delle istanze di un determinato Tipo possano essere trasformate in oggetti manipolabili dalla CRL mediante strutture generiche come stringhe e file. Ad esempio, consideriamo come tipo serializzabile la classe “Persona” una sua istanza, ovvero un oggetto di questo tipo, può esser serializzata per i scopi più vari, ad esempio:
- La trasmissione di una istanza ad un server o ad un client sulla rete
- La persistenza di una istanza su file system
e così via…
La serializzazione avviene considerando un set di componenti base da utilizzare i quali sono : un serializzatore e una fonte di input dei dati. La fonte dei dati può essere: uno stream o una stringa. Il serializzatore ci permette di definire il tipo di serializzazione che vogliamo usare nel nostro software: i tipi più famosi di serializzazione sono Binario, XML e JSON.
In C# abbiamo due “modi di pensare” della serializzazione, in sostanza, si può decidere di definire un contratto chiamato DataContract oppure di serializzare integralmente l’oggetto, annotando solo le variazioni richieste.
DataContract
La classe DataContract permette di serializzare le classi mediante “un contratto” da definire all’interno delle stesse.
La definizione delle classi avviene mediante annotazioni, indipendentemente dalla visibilità degli elementi annotati, in particolare si usano:
- L’annotazione DataContract, che definisce il contratto di serializzazione dell’oggetto in elementi atomici
- L’annotazione DataMember che definisce un elemento della serializzazione.
A seguire un esempio di classe annotata mediante DataContract.
[csharp]
[DataContract(Name = "Persona", Namespace = "http://www.persona.com")]
class Persona
{
[DataMember(Order =1, Name ="NomeDiBattesimo")]
public string nome {
get { return _nome; }
set { _nome = value; }
}
private string _nome;
[DataMember(Order = 2,IsRequired = false,Name ="Cognome", EmitDefaultValue = true)]
public string cognome
{
get;
set;
}
[/csharp]
L’attributo DataContract può avere le seguenti opzioni:
- Name : Attribuisce un nome la contratto utilizzato, risulta utile per evitare conflitti di nomi.
- NameSpace : Attribuisce il namespace da utilizzare, risulta utile nei file XML.
- IsReference : Permette di istruire il serializzatore affinché vengano gestiti i riferimenti circolari all’interno del file XML.
Considera, ad esempio, che l’oggetto persona abbia il riferimento ad un elenco di persone associate: in questo caso due persone (ad esempio dei figli) possono essere associati ad una sola persona (il padre) e pertanto verrà visualizzato un attributo nel file XML risultante che collega il figlio con il suo padre.
L’attributo DataMember può avere le seguenti opzioni:
- Order : Permette di definire l’ordine di apparizione rispetto al padre del nodo, nel nostro esempio, nome apparirà prima di cognome.
- Name : Attribuisce l’etichetta visualizzata dal membro.
- EmitDefaultValue : permette di utilizzare comunque il membro anche se non valorizzato o presente, considerando il valore di default.
- IsRequired : se il campo non è valorizzato in fase di deserializzazione (lettura dalla sorgente), viene lanciata un’eccezione. In fase di serializzazione l’utilizzo dev’essere in accordo con l’opzione EmitDefaultValue: ovvero se il campo è obbligatorio (IsRequired è true) allora dev’essere valorizzato nell’oggetto oppure EmitDefaultValue dev’essere true.
Con il DataContract possiamo ottenere la serializzazione in due differenti tipologie di formato: XML e JSON ottenibili rispettivamente con le classi DataContractSerializer e DataContractJsonSerializer.
Entrambe le classi sono specializzazioni della classe astratta XmlObjectSerializer, per entrambe le classi bisogna passare l’oggetto Type relativo all’oggetto da serializzare.
[csharp]
DataContractSerializer dcs = new DataContractSerializer(typeof(Persona));
Persona p = new Persona();
p.cognome = "CognomePersona";
p.nome = "NomePersona";
p.Indirizzo = "via dei martiri 28";
FileStream fs = File.OpenWrite("serializzato.xml");
dcs.WriteObject(fs,p);
fs.Flush();
fs.Close();
[/csharp]
In questo caso abbiamo scelto di scrivere su file system l’oggetto persona, leggendo il file mediante File.OpenRead
e utilizzando il metodo ReadObject possiamo leggere il file. Osserva che è necessario un Cast per convertire l’oggetto in modo tale da assegnarlo ad un reference del tipo “Persona.”
[csharp]
FileStream fsr = File.OpenRead("serializzato.xml");
Persona per = (Persona)dcs.ReadObject(fsr);
[/csharp]
Nel caso dobbiamo creare dei file JSON invece dei file XML, dobbiamo semplicemente sostituire la classe DataContractSerializer con la classe DataContractJsonSerializer.
NetDataContractSerializer
L’ultimo esempio di serializzatore con contratto che vedremo è il : NetDataContractSerializer.
Questo serializzatore è molto simile al DataContractSerializer, infatti serializza gli XML con una limitazione: vi p bisogno che il tipo serializzato è presente nella CLR che deserializza il file.
La serializzazione è simile, eccetto per il fatto che non vi è bisogno di passare il tipo da serializzare nel costruttore, tale tipo infatti dev’essere presente nel software. Il serializzatore è in grado di serializzare gli oggetti con
DataContractAttribute oppure con i SerializableAttribute e serializza anche i tipi che implementano ISerializable (infatti è possibile usare Serialize e Deserialize).
[csharp]
NetDataContractSerializer dcs = new NetDataContractSerializer();
….
FileStream fs = File.OpenWrite("serializzato.xml");
dcs.WriteObject(fs,p);
…
[/csharp]
Ometto per brevità la deserializzazione che è la medesima.
Serializzazione senza contratto
Precedente alla serializzazione con DataContract esiste anche la serializzazione basata sulla struttura degli oggetti. Tale tipo di serializzazione è decisamente più macchinosa, ma permette di avere maggior controllo sulla serializzazione.
Normalmente si possono serializzare gli oggetti in due formati, in Binario e in XML rispettivamente con il BinaryFormatter e il XmlSerializer.
BinaryFormatter
Tale tecnica consiste nel salvare le classi utilizzando il formato binario, in questo caso il serializzatore salverà integralmente l’oggetto affinché possa essere recuperato, contrariamente ai metodi con DataContract il metodo da utilizzare sono Serialize e Deserialize.
Innanzitutto aggiorniamo la classe Persona affinché sia pulita per la serializzazione senza contratto.
[csharp]
[Serializable]
public class Persona
{
public string nome {
get { return _nome; }
set { _nome = value; }
}
private string _nome;
public string cognome {
get { return _cognome; }
set { _cognome = value; }
}
private string _cognome;
public string Indirizzo
{
get {return indirizzo;}
set {indirizzo = value;}
}
private string fattiSpece;
public void setMiaFattispece(String str) {
fattiSpece = str;
}
private string indirizzo;
}
static void SerializzazioneBinaria(){
Persona p = new Persona();
p.cognome = "Ronaldo";
p.nome = "Cristiano";
p.Indirizzo = "Napoli, via dei martiri 1";
p.setMiaFattispece("assente");
//creo lo stream di scrittura
FileStream fs = File.OpenWrite("serializzato.bin");
BinaryFormatter b = new BinaryFormatter();
b.Serialize(fs,p);
fs.Flush();
fs.Close();
}
FileStream fsr = File.OpenRead("serializzato.bin");
Persona np = (Persona)b.Deserialize(fsr);
fsr.Close();
[/csharp]
In questo caso otterremo che l’oggetto verrà serializzato nel suo complesso, infine notiamo che l’uso dell’attributo Serializable è obbligatorio per effettuare la serializzazione binaria.
Infine se non vogliamo serializzare alcuni attributi pubblici è possibile utilizzare l’attributo NonSerialized.
XmlSerializer
Operativamente il serializzatore XmlSerializer è molto simile al BinaryFormatter.
[csharp]
XmlSerializer dcs = new XmlSerializer(typeof(Persona));
Persona p = new Persona();
p.cognome = "Milik";
p.nome = "Arek";
p.Indirizzo = "via dei martiri 13";
p.setMiaFattispece("assente");
Console.WriteLine(p.ToString());
FileStream fs = File.OpenWrite("serializzato.xml");
dcs.Serialize(fs,p);
fs.Flush();
fs.Close();
FileStream fsr = File.OpenRead("serializzato.xml");
Persona per = (Persona)dcs.Deserialize(fsr);
Console.WriteLine("Deserializzato da file");
Console.WriteLine(per.ToString());
Console.ReadKey();
fsr.Close();
[/csharp]
Facendo girare l’esempio osserveremo una grande prima differenza, il field fattiSpecie non verrà serializzato, perché sono serializzati soltanto gli attributi pubblici.
Inoltre XmlSerializer permette di definire degli attributi per regolare in maniera fine la serializzazione:
- XmlRootAttribute, permette di definire gli attributi del nodo root dell’oggetto, in particolare si possono utilizzare le seguenti opzioni:
- ElementName, permette di definire il nome del nodo da visualizzare come root.
- Namespace, permette di definire il namespace da utilizzare.
- IsNullable, permette di impostare, tramite true o false, se l’elemento può essere nullo.
- DataType, preleva o setta, tramite uno schema XSD da utilizzare nella serializzazione.
- XmlElement, permette di impostare il campo come un elemento dell’albero XML.
- ElementName, permette di definire il nome del nodo da visualizzare nel file XML.
- DataType, permette di impostare, tramite una stringa, il tipo dell’elemento.
- Form, permette di attribuire un valore che permette di esprimere se un elemento è qualificato.
- IsNullable, permette di impostare, tramite true o false, se l’elemento può essere nullo,
- Namespace, permette di definire il namespace da utilizzare.
- Order, permette di definire l’ordine di apparizione del campo all’interno del file serializzato.
- Type, permette di impostare, tramite una stringa, il tipo dell’elemento.
- XmlAttribute, permette di impostare il campo come un attributo del nodo radice. Se si vuole impostare l’attributo ad un nodo intermedio, il nodo itermedio dev’essere convertito a classe ed effettuare materialmente una inclusione.
- ElementName, permette di definire il nome dell’attributo del nodo.
- DataType, preleva o setta, tramite uno schema XSD da utilizzare nella serializzazione.,
- Form, permette di attribuire un valore che permette di esprimere se un elemento è qualificato.
- Namespace, permette di definire il namespace da utilizzare.
- Type, permette di impostare, tramite una stringa, il tipo dell’elemento.
- XmlIgnore, permette di ignorare il campo dell’oggetto anche se questo è public.
Differenze tra DataContractSerializer e XmlSerializer
A questo punto è lecito porsi una domanda: quando conviene utilizzare un contratto?
La risposta (come sempre nel caso dell’informatica) è dipende.
Sostanzialmente nel caso degli XmlSerializer c’è la possibilità di dichiarare gli attributi utilizzando il tag “XmlAttribute” cosa che non è possibile effettuare nel DataContract, tuttavia il DataContract permette di passare da JSON a XML con molta semplicità.
Un caso particolare, il JavaScriptSerializer
Il JavaScriptSerializer è un serializzatore senza contratto che permette di effettuare le operazioni di serializzazione di JSON su stringa. L’utilizzo di tale serializzatore è scoraggiato da Microsoft ed è utilizzato solo per fini interni al framework .Net.
Vediamone l’utilizzo:
[csharp]
JavaScriptSerializer dcs = new JavaScriptSerializer();
StringBuilder fs = new StringBuilder();
Persona p = new Persona();
p.cognome = "Pavoletti";
p.nome = "Leonardo";
p.Indirizzo = "via dei martiri 13";
p.setMiaFattispece("assente");
Console.WriteLine(p.ToString());
dcs.Serialize(p, fs);
string fsr = fs.ToString();
Persona per = (Persona)dcs.Deserialize(fsr, typeof(Persona));
[/csharp]
La serializzazione avviene seguendo le annotazioni utilizzate per la classe XMLSerializer.
Pertanto quest’ultimo modo di serializzare che vediamo, rende la serializzazione senza contratto ugualmente potente rispetto alla serializzazione con contratto.
Facilitazioni alla serializzazione senza contratto
Opzionalmente è possibile implementare l’interfaccia ISerializable e utilizzare i SerializableAttribute.
ISerializzable
L’interfaccia ISerializzable consente ad una classe di disporre del metodo GetObjectData che permette di eseguire delle operazioni prima della serializzazione al fine di arricchire ulteriormente le informazioni della serializzazione.
SerializableAttribute
L’attributo SerializableAttribute permette di indicare che una classe è Serializzabile, si può abbreviare utilizzando semplicemente Serializzable (che abbiamo visto per la serializzazione binaria).
Inoltre è possibile utilizzare degli attributi che permettono di definire dei metodi da eseguire durante la serializzazione, questi metodi sono:
- OnSerializing, il metodo verrà chiamato prima di effettuare la serializzazione.
- OnSerialized, il metodo chiamato appena terminata la serializzazione.
- OnDeserializing, il metodo chiamato prima di effettuare la deserializzazione.
- OnDeserialized, il metodo chiamato appena terminata la deserializzazione.
[csharp]
[OnSerializing()]
internal void OnSerializingMethod(StreamingContext context)
{
//Utile per impostare dei valori ad-hoc della serializzazione
cognomeLegale = "Cognome";
}
[OnSerialized()]
internal void OnSerializedMethod(StreamingContext context)
{
//utile per resettare i valori dopo la serializzazione
cognomeLegale = "";
}
[OnDeserializing()]
internal void OnDeserializingMethod(StreamingContext context)
{
//utile per impostare dei valori ad-hoc durante la deserializzazione
cognomeLegale = "Cognome";
}
[OnDeserialized()]
internal void OnDeserializedMethod(StreamingContext context)
{
//utile per resettare i valori a valle della deserializzazione
cognomeLegale = "";
}
[/csharp]
Conclusioni
Abbiamo visto che la serializzazione è quel processo per cui si vuole che delle istanze di un determinato Tipo possano essere trasformate in oggetti manipolabili dalla CRL mediante strutture generiche come stringhe e file. Abbiamo visto che vari serializzatori che ci permettono di definire il tipo di serializzazione che vogliamo usare nel nostro software: i tipi più famosi di serializzazione sono Binario, XML e JSON ottenibili con: BinaryFormatter, DataContractSerializer, XmlSerializer e DataContractJsonSerializer.
Abbiamo visto i due “modi di pensare” della serializzazione, ovvero tramite il DataContract oppure senza, infine abbiamo visto le varie “eccezioni” ovvero: JavaScriptSerializer, NetDataContractSerializer.
Ho tentato di essere quanto più sintetico ed essenziale nell’articolo, ma la mole di informazioni è notevole. Se riscontrate degli errori, vi invito ad utilizzare i commenti.
Gent.mo Reggina,
devo dingraziarla apertamente per la sua ottima spiegazione.
Chiarissima e pulita, erano giorni che stavo tentando di capire come usare al meglio le serializzazioni e le loro derive; pur avendo girato in lungo ed in largo anche su siti esteri lei è stato il più chiaro ed esaustivo fra tutti, permettendomi di comprendenre meglio l’argomento.
Ancora ringraziamenti sentiti
Sono lieto che l’articolo ti sia stato utile!