Se abbiamo necessità di generare rappresentazioni in un formato diverso da JSON e XML delle risorse gestite dalla nostra Web API abbiamo la possibilità di creare un nostro media formatter.
Vediamo come sfruttare questa possibilità supponendo di voler generare le voci del nostro glossario in formato CSV.
Per prima cosa creiamo una cartella Formatters all'interno del nostro progetto ed inseriamo una classe ItemCSVFormatter. Come detto all'inizio di questa guida, la creazione della cartella non è strettamente necessaria, ma è opportuno crearla per organizzare meglio i vari elementi del nostro progetto.
Un media formatter è una classe che può derivare da una delle seguenti classi: MediaTypeFormatter
o BufferedMediaTypeFormatter
. La prima utilizza metodi asincroni di lettura e scrittura mentre la seconda supporta un approccio sincrono.
Nel nostro caso utilizzeremo l'approccio sincrono dal momento che risulta più semplice e il blocco del thread chiamante durante le operazioni di I/O è trascurabile viste le esigue dimensioni degli oggetti da serializzare e deserializzare.
Definiamo pertanto la nostra classe nel seguente modo:
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using Glossario.Models;
namespace Glossario.Formatters
{
public class ItemCSVFormatter : BufferedMediaTypeFormatter
{
}
}
Indichiamo poi nel costruttore della classe i media-type supportati:
public ItemCSVFormatter()
{
SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/csv"));
}
Ridefiniamo il metodo CanWriteType()
per indicare quali tipi di oggetto il nostro formatter è in grado di serializzare:
public override bool CanWriteType(System.Type type)
{
if (type == typeof(Item))
{
return true;
}
else
{
Type enumerableType = typeof(IEnumerable<Item>);
return enumerableType.IsAssignableFrom(type);
}
}
Questo metodo riceve un tipo come parametro e restituisce un valore booleano che stabilisce se la classe lo supporta. Come si può vedere dal codice, il nostro media formatter supporta la serializzazione sia di un singolo oggetto che di un elenco di oggetti di tipo Item.
Analogamente, per indicare i tipi di oggetto che il nostro formatter è in grado di deserializzare eseguiamo l'override di CanReadType()
:
public override bool CanReadType(System.Type type)
{
if (type == typeof(Item))
{
return true;
}
else
{
Type enumerableType = typeof(IEnumerable<Item>);
return enumerableType.IsAssignableFrom(type);
}
}
Come possiamo vedere l'implementazione è identica a quella di CanWriteType()
, per cui sarebbe opportuno concentrare il codice in una funzione privata e richiamarla in entrambi i metodi.
Serializzazione e deserializzazione
A questo punto passiamo ad implementare le effettive operazioni di serializzazione e deserializzazione. Per la serializzazione occorre effettuare l'override del metodo WriteToStream(), come nel seguente esempio:
public override void WriteToStream(Type type, object value, Stream stream, HttpContentHeaders contentHeaders)
{
using (var writer = new StreamWriter(stream))
{
var items = value as IEnumerable<Item>;
if (items != null)
{
foreach (var myItem in items)
{
WriteItem(myItem, writer);
}
}
else
{
var singleItem = value as Item;
if (singleItem == null)
{
throw new InvalidOperationException("Cannot serialize type");
}
WriteItem(singleItem, writer);
}
}
stream.Close();
}
In sintesi, il metodo restituisce un elenco di righe o una sola riga CSV in base al valore ricevuto in input. La serializzazione vera e propria per ciascun oggetto viene effettuata dalla funzione WriteItem()
:
private void WriteItem(Item myItem, StreamWriter writer)
{
writer.WriteLine("\"{0}\",\"{1}\",\"{2}\"", Escape(myItem.term),
Escape(myItem.definition), Escape(myItem.category));
}
static char[] _specialChars = new char[] { ',', '\n', '\r', '"' };
private string Escape(object o)
{
if (o == null)
{
return "";
}
string field = o.ToString();
if (field.IndexOfAny(_specialChars) != -1)
{
return String.Format("\"{0}\"", field.Replace("\"", "\"\""));
}
else return field;
}
Come evidenziato dal codice, la funzione WriteItem()
utilizza una funzione Escape()
per la gestione dei caratteri speciali come virgola, ritorno a capo, ecc.
Analogamente, per implementare la deserializzazione ridefiniamo il metodo ReadFromStream()
:
public override object ReadFromStream(Type type, Stream stream, HttpContentHeaders contentHeaders, IFormatterLogger formatterLogger)
{
object result;
using (var reader = new StreamReader(stream))
{
var CSVText = reader.ReadToEnd();
var itemLinesArray = CSVText.Split('\n');
var itemsCount = itemLinesArray.Length;
Item[] itemArray;
if (itemsCount == 1)
{
result = ReadItem(itemLinesArray[0]);
}
else
{
itemArray = new Item[itemsCount];
for (var i = 0; i < itemsCount;i++ )
{
itemArray[i] = ReadItem(itemLinesArray[i]);
}
result = itemArray;
}
}
stream.Close();
return result;
}
Anche in questo caso consideriamo le due situazioni relative al singolo oggetto o ad un elenco di oggetti. La deserializzazione effettiva del singolo oggetto è realizzata dalla funzione ReadItem()
:
private Item ReadItem(string itemLine)
{
var itemArray = itemLine.Replace("\"","").Split(',');
Item myItem = new Item();
myItem.term = itemArray[0];
myItem.definition = itemArray[1];
myItem.category = itemArray[2];
return myItem;
}
A questo punto il nostro media formatter è pronto. Non ci resta che integrarlo nel sistema in modo tale che, in base alle richieste del client, possa rappresentare le voci del nostro glossario in formato CSV. Per far questo è sufficiente inserire la seguente istruzione in corrispondenza al metodo Application_Start()
in Global.asax
:
protected void Application_Start()
{
...
GlobalConfiguration.Configuration.Formatters.Add(new Glossario.Formatters.ItemCSVFormatter());
...
}
Il risultato ottenuto da un client che specifica text/csv nella negoziazione dei contenuti sarà analogo a quello mostrato nella seguente figura: