Press "Enter" to skip to content

Serializzare e deserializzare in JSON

Questo articolo ci mostra come generare un helper per la serializzazione e deserializzazione di oggetti in JSON.

Nei precedenti articoli, abbiamo creato un oggetto che rappresenta i dati necessari a memorizzare una stringa di connessione SQL Server, definendo l’entità abbiamo previsto che potremo avere la necessità di salvare i dati di connessione su disco o su database, pertanto abbiamo adornato le proprietà che ci interessano all’interno dell’entità con gli attributi necessari a poter serializzare la classe in JSON.

Negli articoli storici e nelle librerie Dotnetwork per la versione 2.0 del framework abbiamo spiegato come fare a serializzare un oggetto in XML e creato un helper che ci permette di farlo, vediamo ora come fare lo stesso utilizzando il formato dati JSON.

Come già mostrato nel precedente articolo Diamoci un Contesto abbiamo già inserito nel codice dell’ entity che rappresenta le informazioni per una connessione a database il necessario per poterla serializzare, ne facciamo un riepilogo.

	[DataContract(Name = "SCNI")]
	public class SqlConnectionInfo : ConnectionInfoBase
	{

Una classe può essere serializzata tramite JSON adornando la sua dichiarazione con l’attributo DataContract (Disponibile aggiungendo: using System.Runtime.Serialization), la serializzazione XML salva sempre tutte le property Pubbliche della classe fatto salvo quelle che noi dichiariamo con l’attributo XmlIgnore e quelle prive del Setter (read only). Al contrario, JSON serializza solo le property che vengono adornate dell’ attributo DataMember.

[DataMember(Name = "SRV", EmitDefaultValue = false)]
public string Server
{
	get
	{
		return mServer;
	}
	set
	{
		mServer = value;
		OnPropertyChanged(FLD_Server);
	}
}

Come già indicato, Sia per la classe che per le property abbiamo deciso di utilizzare un Name diverso da quello della classe o della property C# questo per rendere compatto il file dati e perché così se per qualsiasi motivo fosse necessario modificare il nome della classe o delle proprietà in futuro, la cosa non andrebbe a creare problemi alla lettura di file dati memorizzati prima del cambiamento, posto che ovviamente non modifichiamo il nome JSON.

Prima di provare la serializzazione di un oggetto SqlConnectionInfo, aggiungiamo un altra classe, infatti per lavorare con le nostre applicazioni potremmo avere la necessità di usare più di una connessione a SQL Server, pertanto, creiamo una Collection tipizzata per ospitare i nostri oggetti SqlConnectionInfo.

La classe SqlConnectionInfosCollection

using Dnw.Base.Data.SqlServer.Entities;
using System.Collections.ObjectModel;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;

namespace Dnw.Base.Data.SqlServer.Collections
{
	public class SqlConnectionInfosCollection : ObservableCollection<SqlConnectionInfo>
	{
		public SqlConnectionInfo this[string ID]
		{
			get
			{
				SqlConnectionInfo x = null;
				if (this.Any(item => item.ConnectionID == ID))
				{
					x = this.First(item => item.ConnectionID == ID);
				}
				return (x);
			}
		}
		public override string ToString()
		{
			StringBuilder sb = new StringBuilder();
			for (int i = 0; i < this.Count; i++)
			{
				sb.AppendLine(this[i].ToString());
			}
			return (sb.ToString());
		}
	}
}

Abbiamo utilizzato la ObservableCollection generic come base per la nostra collection di informazioni di connessione perché in seguito useremo questa collection in WPF per creare un interfaccia utente che permetta di generare le informazioni di connessione e memorizzarle poi su file. Abbiamo implementato un Indexer che ci permetterà di accedere agli elementi tramite la loro ID univoca ed abbiamo implementato il metodo ToString ai fini dei test.

La classe JsonHelper

Così come avevamo fatto per XML, nella nostra libreria DnwBase, andiamo ad aggiungere una cartella Json e in questa cartella la classe JsonHelper

SqlConnectionInfo_sln

All’interno inseriremo 3 metodi statici per Serializzare e Deserializzare dati in formato Json:

using System;
using System.IO;
using System.Runtime.Serialization.Json;
using System.Text;

namespace Dnw.Base.Json
{
	public static class JsonHelper
	{

La definizione della classe, da notare il namespace System.Runtime.Serialization.Json che contiene tutte le primitive per la creazione e lettura dei dati nel formato.

public static string Serialize(Type objectType, object data)
{
	try
	{

		string ret = null;
		using (MemoryStream ms = new MemoryStream())
		{
			DataContractJsonSerializer ser = new DataContractJsonSerializer(objectType);
			ser.WriteObject(ms, data);
			byte[] json = ms.ToArray();
			ms.Close();
			ret = Encoding.UTF8.GetString(json, 0, json.Length);
		}
		return (ret);

	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		throw;
	}
}

Il primo metodo, genera una stringa contenente l’oggetto in formato JSON utilizzando un memory stream (che ci permette di non dover per forza serializzare su un file o uno stream specifico). Viene utilizzato l’ oggetto DataContractJsonSerializer per creare l’array di dati e viene utilizzato il GetString dell’ encoding per generare la stringa. Attenzione abbiamo usato l’UTF8 che è il formato standard delle stringhe .Net, ma nulla vieta di usare qualsiasi altro encoding, posto che lo facciamo sia in scrittura che in lettura dei files.

public static void Serialize(Type objectType, object data, string fileName)
{
	try
	{
		File.WriteAllText(fileName, Serialize(objectType, data));
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		throw;
	}
}

Il secondo metodo, l’overload per scrivere su file, non aggiunge nulla se non la scrittura della stringa su un file di testo.

public static object Deserialize(Type objectType, string fileNameOrData, bool isFile = true)
{
	try
	{
		string data = null;
		if (isFile)
		{
			data = File.ReadAllText(fileNameOrData);
		}
		else
		{
			data = fileNameOrData;
		}
		object deserializedData = null;
		using (MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(data)))
		{
			DataContractJsonSerializer ser = new DataContractJsonSerializer(objectType);
			deserializedData = ser.ReadObject(ms);
			ms.Close();
		}
		return deserializedData;
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		throw;
	}
}

Il metodo per la lettura e riconversione dei dati Json, in questo caso non possiamo fare un overload perché la lettura da file e quella da stringa avrebbero la medesima Signature (il nome file e il contenuto del file sono sempre delle stringhe) pertanto abbiamo inserito un booleano che indica se stiamo passando al metodo un file da leggere o una stringa di dati. Ora abbiamo quanto serve per testare la serializzazione della nostra collection.

Il progetto di test

Nel progetto di test, in modo molto semplice testiamo la serializzazione  creando un interfaccia minimale:

SqlConnInfoJson_01

Abbiamo una textbox ove inserire un nome di file ove salvare o da cui leggere i dati, un pulsante per selezionare un file se necessario, quattro pulsanti per testare i metodi di serializzazione, due aree di testo ove mostrare il risultato. Vediamo quindi il codice dietro ai cinque bottoni:

private void btnGetFile_Click(object sender, RoutedEventArgs e)
{
	try
	{

		OpenFileDialog ofd = new OpenFileDialog();
		ofd.Title = "Nome del file da aprire";
		ofd.FileName = "";
		ofd.DefaultExt = ".json";
		ofd.Multiselect = false;
		ofd.Filter = "Json files (*.json)|*.json|Dnw Hidden (*.dnwx)|*.dnwx|Tutti i files (*.*)|*.*";
		ofd.FilterIndex = 1;
		ofd.CheckFileExists = false;
		ofd.CheckPathExists = false;
		bool? ret = ofd.ShowDialog();
		if (ret.HasValue && ret.Value)
		{
			this.txtFileName.Text = ofd.FileName;
		}
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		MessageBox.Show(ex.Message);
	}
}

Primo bottone, acquisisce un nome di file utilizzando la OpenFileDialog, è la finestra di sistema, non un oggetto WPF, pertanto il suo uso implica quello del namespace Microsoft.Win32, volendo utilizzare solo WPF, c’è una libreria gratuita di Microsoft su Codeplex che si chiama Windows API Codepack che può essere scaricata e compilata e fornisce delle Dialog WPF equivalenti a quelle di sistema, con inoltre una serie di funzionalità in più.

private void LoadOne()
{
	mConnections = new SqlConnectionInfosCollection();
	SqlConnectionInfo con = new SqlConnectionInfo();
	con.ConnectionID = "ONE";
	con.ConnectionType = "ADMIN";
	con.Description = "Connection number one";
	con.Server = "localhost";
	con.Database = "master";
	con.Trusted = true;
	con.Position = 1;
	mConnections.Add(con);

Ci sono due funzioni di esempio che generano ciascuna 3 SqlConnectionInfo per permetterci di testare la serializzazione. Si chiamano fantasiosamente LoadOne e LoadTwo.

private void btnWrite_Click(object sender, RoutedEventArgs e)
{
	try
	{
		LoadOne();
		this.txtStatus.Text = "";
		if (!this.txtFileName.Text.XDwIsNullOrTrimEmpty())
		{
			JsonHelper.Serialize(typeof(SqlConnectionInfosCollection), mConnections, this.txtFileName.Text);
			this.txtResult.Text = JsonHelper.Serialize(mConnections.GetType(), mConnections);
			this.txtStatus.Text = "File written" + this.txtFileName.Text;
		}
		else
		{
			MessageBox.Show("You need to set a file name to serialize the data.");
		}
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		MessageBox.Show(ex.Message);
	}
}

Serializziamo sul file da noi indicato sulla textbox il contenuto della collezione che abbiamo inserito come Variabile member della finestra di test, quello che sarà scritto su file è quanto indicato qui sotto:

SqlConnInfoJson_02

Per effettuare correttamente il test, scriviamo sul file e scriviamo anche sulla textbox.

SqlConnInfoJson_06

private void btnWriteHidden_Click(object sender, RoutedEventArgs e)
{
	try
	{
		LoadTwo();
		this.txtStatus.Text = "";
		if (!this.txtFileName.Text.XDwIsNullOrTrimEmpty())
		{
			string data = JsonHelper.Serialize(typeof(SqlConnectionInfosCollection), mConnections);
			string result = EncDec.Encrypt(data);
			this.txtResult.Text = result;
			string file = this.txtFileName.Text.Replace(Path.GetExtension(this.txtFileName.Text), ".dnwx");
			File.WriteAllText(file, result);
			this.txtStatus.Text = "File written" + file;
		}
		else
		{
			MessageBox.Show("You need to set a file name to serialize the data.");
		}
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		MessageBox.Show(ex.Message);
	}
}

In questo secondo test, usiamo la classe helper EncDec, per crittografare il file in cui memorizziamo i dati JSON, ma utilizziamo sempre lo stesso metodo per creare la stringa con i dati Serializzati.

SqlConnInfoJson_04

Potete anche notare, che ho usato un estensione personalizzata (.dnwx) al posto del .json per il file generato.

private void btnRead_Click(object sender, RoutedEventArgs e)
{
	try
	{
		this.txtStatus.Text = "";
		if (!this.txtFileName.Text.XDwIsNullOrTrimEmpty())
		{
			if (File.Exists(this.txtFileName.Text))
			{
				SqlConnectionInfosCollection newColl = 
				    (SqlConnectionInfosCollection)JsonHelper.Deserialize(
				    typeof(SqlConnectionInfosCollection), this.txtFileName.Text);
				this.txtResult.Text = newColl.ToString();
				this.txtStatus.Text = "File Read" + this.txtFileName.Text;
			}
		}
		else
		{
			MessageBox.Show("You need to set a file name from which to deserialize the data.");
		}
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		MessageBox.Show(ex.Message);
	}
}

Usiamo la funzione di deserializzazione omettendo il parametro facoltativo che per default indica che stiamo fornendo un nome file al metodo.

SqlConnInfoJson_03

Abbiamo generato la lista salvata sul file in chiaro.

private void btnReadHidden_Click(object sender, RoutedEventArgs e)
{
	try
	{
		this.txtStatus.Text = "";
		if (!this.txtFileName.Text.XDwIsNullOrTrimEmpty())
		{
			if (File.Exists(this.txtFileName.Text))
			{
				string file = this.txtFileName.Text.Replace(Path.GetExtension(this.txtFileName.Text), ".dnwx");
				string result = EncDec.Decrypt(File.ReadAllText(file));
				SqlConnectionInfosCollection newColl = 
				    (SqlConnectionInfosCollection)JsonHelper.Deserialize(
				    typeof(SqlConnectionInfosCollection), result, false);
				this.txtResult.Text = newColl.ToString();
				this.txtStatus.Text = "File Read" + file;
			}
		}
		else
		{
			MessageBox.Show("You need to set a file name from which to deserialize the data.");
		}
	}
	catch (Exception ex)
	{
		EventLogger.SendMsg(ex);
		MessageBox.Show(ex.Message);
	}
}

Per rileggere i dati crittografati, acquisiamo il contenuto del file crittografato, li decrittografiamo con l’uso dell’helper apposito e deserializziamo la stringa ottenuta con l’helper JSON per ottenere il contenuto della seconda collection generata.

SqlConnInfoJson_05

Conclusioni

In questo esempio abbiamo visto come predisporre una classe dati per essere serializzata in formato JSON. Abbiamo creato un helper generico per Serializzare e deserializzare qualsiasi oggetto in formato JSON, ed infine abbiamo testato le funzionalità implementate grazie alla classe SqlConnectionInfo e alla classe helper per la crittografia.

Il codice del progetto esempio può essere scaricato al link seguente:

La parte di uso comune, e le classi riusabili in più progetti sono disponibili al link seguente:

Per qualsiasi commento, domanda, approfondimento, usate pure il link al modulo di contatto in cima alla pagina.