Dopo un mese dall’evento di Pordenone, riesco finalmente ad avere il tempo di iniziare una breve serie di post dedicati alla serializzazione delle entity. Inizierò con l’helper di serializzazione già discusso in passato, ma ora riveduto e corretto usando il framework 4.0.
Per persistere su disco una classe C# (oVB) bisogna che questa classe sia creata con alcuni, semplici accorgimenti che la rendano Serializzabile. Serializzazione è una parola che è iniziata ad arrivare ai miei orecchi attorno al 2003, alle prime conferenze su .NET, è una parola derivata dall’inglese Serialization, non esiste sul Devoto Oli, il suo significato è persistere lo stato di un oggetto che si trova nella memoria di un programma in esecuzione in modo tale da poter rigenerare l’oggetto nello stesso stato rileggendo le informazioni salvate su un disco (o altro analogo supporto di memoria di massa.)
Le classi Entity, sono delle classi che non hanno nulla di particolare se non il fatto che rappresentano dei dati strutturati, pertanto vengono considerate tali tutte le classi che compongono ad esempio un modello dati, semplice, usato con LinqToSql oppure con EntityFramework, pertanto sono quelle che con maggiore probabilità hanno la necessità di essere serializzate.
In .NET, è possibile serializzare dati in 2 modalità, la serializzazione binaria e la serializzazione XML. Tutti i tipi di dati base di .NET sono serializzabili, pertanto, qualsiasi classe che contiene solo dati di questo tipo è serializzabile in modo diretto, senza particolari modifiche se non che è indispensabile che la classe implementi un costruttore senza parametri.
Che cosa viene serializzato automaticamente da .NET di un oggetto? Tutte le property Pubbliche, perché le sole property pubbliche? perché si assume che le variabili private possano essere rigenerate senza necessità di essere serializzate. Ma è ovviamente possibile effettuare un Override della serializzazione automatica e serializzare ciò che si vuole e come si vuole.
In questa serie di articoli esamineremo solo la serializzazione standard e tutto quello che si può pilotare in essa utilizzando poche semplici funzionalità a nostra disposizione.
La classe SerializeHelper
Questa classe fornisce tutti i servizi di serializzazione sia per oggetti semplici che per oggetti compositi (collezioni od oggetti contenenti tipi di dati non semplici). Utilizza le classi del namespace System.Xml e System.Xml.Serialization. Vediamone quindi i vari metodi:
BuildReader
public static XmlTextReader BuildReader(string pXmlString) { NameTable nt = new NameTable(); XmlNamespaceManager nsmgr = new XmlNamespaceManager(nt); nsmgr.AddNamespace("bk", "urn:sample"); XmlParserContext context = new XmlParserContext(null, nsmgr, null, XmlSpace.None); return (new XmlTextReader(pXmlString, XmlNodeType.Element, context)); }
Questo primo metodo, come dice il suo nome, crea un XmlTextReader, che con il corrispondente XmlTextWriter sono gli stream di base per la lettura e scrittura di dati XML. Il metodo qui scritto, è stato disegnato per permetterci di trasformare una stringa (ad esempio un campo di un database) in uno stream Xml. Si occupa di generare un contesto “fittizio” per così dire, per poter creare l’XmlTextReader in grado di fornire i dati Xml anche se l’input è una semplice stringa, non uno stream dati vero e proprio.
DeserializeFromString
public static object DeserializeFromString(Type pTypeToDeserialize, string pXmlString) { object ret = null; using (XmlReader xr = BuildReader(pXmlString)) { XmlSerializer serializer = new XmlSerializer(pTypeToDeserialize); ret = serializer.Deserialize(xr); xr.Close(); } return (ret); } public static object DeserializeFromString(Type pTypeToDeserialize,
Type[] pExtraTypes, string pXmlString) { object ret = null; using (XmlReader xr = BuildReader(pXmlString)) { XmlSerializer serializer = new XmlSerializer(pTypeToDeserialize, pExtraTypes); ret = serializer.Deserialize(xr); xr.Close(); } return (ret); }
Questi due metodi, costruiti l’uno per una classe contenente solo dati semplici, l’altro per una classe contenente tipi di dati complessi, utilizzano il metodo precedente per creare uno stream dati che poi viene passato alla classe XmlSerializer e permette di ottenere l’oggetto originalmente serializzato.
DeserializeFromFile
public static object DeserializeFromFile(Type pTypeToDeserialize, string pPath) { object ret = null; using (XmlTextReader reader = new XmlTextReader(pPath)) { // Occorre un'istanza della classe XmlSerializer XmlSerializer serializer = new XmlSerializer(pTypeToDeserialize); // e questo é tutto ciò che serve per leggere i dati dal formato XML ret = serializer.Deserialize(reader); reader.Close(); } return (ret); } public static object DeserializeFromFile(Type pTypeToDeserialize,
Type[] pExtraTypes, string pPath) { object ret = null; using (XmlTextReader reader = new XmlTextReader(pPath)) { // Occorre un'istanza della classe XmlSerializer XmlSerializer serializer = new XmlSerializer(pTypeToDeserialize, pExtraTypes); // e questo é tutto ciò che serve per leggere i dati dal formato XML ret = serializer.Deserialize(reader); reader.Close(); } return (ret); }
La deserializzazione da file è più semplice in quanto l’XmlTextreader può essere direttamente istanziato a partire dal nome di un file. I due overload di questo metodo permettono di specificare al deserializzatore quali sono le classi coinvolte, nel caso di oggetti non semplici. Vedremo il loro uso quando deserializzeremo le collection, ma allo stesso modo sono utili per la deserializzazione di oggetti complessi, che formano modelli con relazioni.
SerializeToFile
public static void SerializeToFile(string pPath, object pObjToSerialize,
Type pTypeToSerialize, bool pNoNamespaces, string pPrefix) { FileInfo fInfo = new FileInfo(pPath); if (!Directory.Exists(fInfo.DirectoryName)) { Directory.CreateDirectory(fInfo.DirectoryName); } using(XmlTextWriter writer = new XmlTextWriter(pPath, Encoding.UTF8)) { // write a readable file writer.Formatting = Formatting.Indented; writer.Indentation = 4; // Occorre un'istanza della classe XmlSerializer XmlSerializer serializer = new XmlSerializer(pTypeToSerialize); XmlSerializerNamespaces ns = new XmlSerializerNamespaces(); string prefix = pPrefix != null ? pPrefix : string.Empty; if (pNoNamespaces) { ns.Add(prefix, TXT_Namespace); } // e questo é tutto ciò che serve per persistere i dati serializer.Serialize(writer, pObjToSerialize, ns); writer.Close(); } } public static void SerializeToFile(string pPath, object pObjToSerialize, Type pTypeToSerialize, Type[] pExtraTypes, bool pNoNamespaces, string pPrefix) { FileInfo fInfo = new FileInfo(pPath); if (!Directory.Exists(fInfo.DirectoryName)) { Directory.CreateDirectory(fInfo.DirectoryName); } using(XmlTextWriter writer = new XmlTextWriter(pPath, Encoding.UTF8)) { // write a readable file writer.Formatting = Formatting.Indented; writer.Indentation = 4; //writer.Settings.OutputMethod = XmlOutputMethod. // Occorre un'istanza della classe XmlSerializer XmlSerializer serializer = new XmlSerializer(pTypeToSerialize, pExtraTypes); XmlSerializerNamespaces ns = new XmlSerializerNamespaces(); string prefix = pPrefix != null ? pPrefix : string.Empty; if (pNoNamespaces) { ns.Add(prefix, TXT_Namespace); } // e questo é tutto ciò che serve per persistere i dati serializer.Serialize(writer, pObjToSerialize, ns); writer.Close(); } }
Per la serializzazione, oltre ai due overload che ci danno modo di dare informazioni al serializzatore sulle classi coinvolte, abbiamo aggiunto alcuni parametri che servono per la formattazione: pNoNamespaces e pPrefix.
Innanzi tutto possiamo notare come Creando l’XmlTextWriter, la classe che effettua la serializzazione, possiamo decidere l’encoding della stringa, pertanto, in base alle nostre esigenze possiamo cambiare l’encoding del file generato. Possiamo inoltre controllare la formattazione del file Xml generato usando l’opzione Formatting che ci da modo di indicare come deve essere formattato il testo, alcune altre property permettono di decidere l’ampiezza dell’indentazione e il carattere da utilizzare.
l’uso di XmlSerializerNamespaces ci permette di evitare la verbosa serie dei namespaces inseriti all’apertura della serializzazione della classe:
<Serializzabile xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” xmlns:xsd=”http://www.w3.org/2001/XMLSchema”>
Sostituendola con:
<Competences xmlns=”http://www.dotnetwork.it”>
Oppure, usando il flag pNoNamespaces in:
<Competences>
Questo flag è opportuno utilizzarlo per creare classi neutre da utilizzare per lo scambio dati fra applicazioni di aziende diverse, mentre se vogliamo fare in modo che le nostre classi siano marcate in modo specifico, lasciamo invece il namespace che abbiamo scelto per la nostra azienda, in modo tale che sia necessario specificamente implementarlo nelle classi da noi prodotte in modo tale da renderle univoche rispetto a quelle prodotte da altri.
Il parametro pPrefix ha invece lo scopo di assegnare un prefisso agli elementi delle nostre classi.
<dnw:Programmer xmlns:dnw=”http://www.dotnetwork.it”>
<dnw:Skills>
In questo modo possiamo specificare esattamente l’uso dei nostri oggetti senza dover reinserire il namespace completo.
SerializeToString
public static string SerializeToString(object pObjToSerialize,
Type[] pExtraTypes, bool pNoNamespaces, string pPrefix) { string ret = string.Empty; using (MemoryStream stream = new MemoryStream()) { XmlSerializer serializer = new XmlSerializer(pObjToSerialize.GetType(), pExtraTypes); XmlSerializerNamespaces ns = new XmlSerializerNamespaces(); string prefix = pPrefix != null ? pPrefix : string.Empty; if (pNoNamespaces) { ns.Add(prefix, TXT_Namespace); } serializer.Serialize(stream, pObjToSerialize, ns); ret = Encoding.UTF8.GetString(stream.ToArray()); stream.Close(); } return (ret); } public static string SerializeToString(object pObjToSerialize, bool pNoNamespaces, string pPrefix) { string ret = string.Empty; using (MemoryStream stream = new MemoryStream()) { XmlSerializer serializer = new XmlSerializer(pObjToSerialize.GetType()); XmlSerializerNamespaces ns = new XmlSerializerNamespaces(); string prefix = pPrefix != null ? pPrefix : string.Empty; if (pNoNamespaces) { ns.Add(prefix, TXT_Namespace); } serializer.Serialize(stream, pObjToSerialize, ns); ret = Encoding.UTF8.GetString(stream.ToArray()); stream.Close(); } return (ret); }
L’ultimo metodo, sempre con due overload, permette la serializzazione di un oggetto in Xml su una stringa, per poterlo fare dobbiamo utilizzare uno Stream, visto che dobbiamo poi trasformarlo in una stringa, utilizziamo un Memory Stream, per non dover generare un file. La procedura per la generazione dell’Xml, è identica a quella per i files.