WCF Data Services - Prima Parte
Scritto da
Pietro Libro
il
mercoledì 21 dicembre 2011
(aggiornato il 21 dicembre 2011)
Linguaggio:
•
Framework:
•
Livello: 200
Download sorgenti
I WCF Data Services (noti fino a qualche tempo fa come ADO.NET
Data Services) permettono di creare servizi basati sul protocollo
OData (Open Data Protocol) per esporre dati su Web utilizzando REST
(Representational State Transfer). L'utilizzo di OData permette
l'accesso ai servizi da qualsiasi client che lo supporti,
attraverso lo scambio dati in formato JSON, Testo o XML
(Atom). Gli scenari di utilizzo dei WFC Data Services sono
molteplici dato che i "consumatori" dei servizi possono essere
applicazioni Web, Silverlight, Windows Form, WPF, mobile ed i dati
esposti non devono per forza provenire da una base dati. Prima di
vedere qualche esempio è necessario spendere qualche parola in
merito al protocollo OData e REST.
OData
L'Open Data Protocol è un protocollo Web che permette
l'interrogazione e l'aggiornamento dati basandosi su tecnologie Web
quali HTTP, Atom Publishing Protocol (AtomPub) e JSON, fornendo
così l'accesso ad una moltitudine di applicazioni e servizi. OData
è rilasciato sotto licenza Open Specification Promise ed Il
sito ufficiale di riferimento è www.odata.org. E' possibile
iscriversi ad una mailing list per avere maggiori delucidazioni sul
protocollo ed eventualmente per discutere l'evoluzione del
protocollo nel tempo. Tra le varie applicazioni che espongono i
dati in formato OData, troviamo applicazioni quali: SharePoint
2010, Microsoft SQL Azure, Windows Azure Table Storage, Reporting
Services, OData per Team Foundation Server 2010 Beta, giusto per
citare alcuni dei prodotti Microsoft, ma nella lista presente
all'indirizzo http://www.odata.org/producers abbiamo anche nomi
come IBM WebShpere. Dal sito di OData è possibile scaricare
un bel po' di materiale informativo tra cui l'SDK relativo
comprensivo di un certo numero di librerie per poter interagire con
.NET, Windows Phone 7, Silverlight 4, PHP, Objective-C ecc. (elenco
completo: http://www.odata.org/developers/odata-sdk). Il
materiale a disposizione sul sito dovrebbe soddisfare eventuali
ulteriori curiosità sull'argomento.
REST (Representational State Transfer)
Per parlare in dettaglio di REST, ovviamente non basterebbe un
solo articolo, ma proviamo a dare qualche definizione ed una
panoramica generale di questa semantica, al fine di una migliore
comprensione del funzionamento dei WCF Data Services. Per chi
volesse approfondire nei dettagli il discorso relativo
all'architettura ed al funzionamento di REST può fare riferimento
al capitolo 5 del documento Architectural Styles and the Design
of Network-based Software Architectures di Roy Thomas
Fielding, documento tra linkato anche da MDSN. Il capitolo può
essere visionato qui:
http://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm
e scaricabile interamente in PDF qui:
http://www.ics.uci.edu/~fielding/pubs/dissertation/fielding_dissertation_2up.pdf.
Le condizioni di utilizzo del documento sono riportate qui:
http://www.ics.uci.edu/~fielding/pubs/dissertation/faq.htm.
REST è un'architettura software che permette la creazione di
applicazioni Web (e non solo) utilizzando per la manipolazione
delle risorse i metodi GET, POST, PUT e DELETE presenti nella
definizione del protocollo HTTP. Come precedentemente accennato,
questo terminato è stato coniato da Roy Thomas Fielding (tra
l'altro co-autore del protocollo HTTP) specificando un sistema che
permette di identificare e descrivere le "risorse" presenti nel
Web. Una "risorsa" è il fulcro sul quale ruota questo
paradigma: una pagina web è una risorsa, un'immagine contenuta in
una pagina è una risorsa, tutto ciò che può essere univocamente
identificato tramite un indirizzo internet può essere considerata
una risorsa (Web). Possiamo interagire con le risorse utilizzando ,
ad esempio i metodi GET e PUT.
Una stessa risorsa può essere serializzata in formati diversi come
XML, JSON, HTML o TXT secondo delle esigenze ed il formato a noi
più comodo.
I punti chiave dell'architettura REST sono:
- Stato e funzionalità divisi in Risorse Web
- Ogni risorsa è unica ed indirizzabile usando un link
ipertestuale
- Protocollo Client-Server, Stateless, Cachable e ad livelli
WCF Data Services
L'architettura dei WCF Data Services è ben illustrata dalla
figura sottostante. Attraverso questi servizi possiamo esporre i
dai in formato OData a svariati tipi di applicazioni.

Dopo questa breve panoramica iniziamo a sporcarci le mani con un
po' di codice. Apriamo una nuova istanza di Visual Studio 2010 e
creiamo una nuova soluzione a partire da un template ASP.NET Web
Application (ovviamente data la natura dei WCF Data Services) che
rinominiamo in DomusDotNet.WcfDataServices:

Aggiungiamo subito un nuovo item di tipo WCF Data Service che
chiameremo DomusDotNetService:

A questo punto la nostra soluzione dovrebbe essere simile alla
figura seguente (VS 2010 aggiunge automaticamente tutti i
riferimenti affinché il progetto possa essere compilato
correttamente):

Eliminiamo i file About.aspx, Default.aspx e
Site.Master e le cartelle Account ed App_Data ed
impostiamo il file con estensione .svc come Start Page
della nostra Web Application. Se provassimo a compilare verrebbe
generato un errore di compilazione perché dopo aver aggiunto l'item
WCF Data Service al progetto, VS ha creato la classe
DomusDotNetService la quale eredita da una classe generica
DataService<T>, dove per il momento il tipo T non è
specificato:
public class DomusDotNetService : DataService< /* TODO: put your data source class name here */ >
{
// This method is called only once to initialize service-wide policies.
public static void InitializeService(DataServiceConfiguration config)
{
// TODO: set rules to indicate which entity sets and service operations are visible, updatable, etc.
// Examples:
// config.SetEntitySetAccessRule("MyEntityset", EntitySetRights.AllRead);
// config.SetServiceOperationAccessRule("MyServiceOperation", ServiceOperationRights.All);
config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
}
}
A breve il significato delle righe commentate sarà più chiaro.
Per gli esempi dell'articolo utilizzeremo un semplice Object
Model come specificato dal seguente Class Diagram:

Decoriamo le classi Department ed Employee in
questo modo:
[DataServiceKey("Id")]
public class Employee
{
…
}
[DataServiceKey("Id")]
public class Department
{
…
}
Dove l'attributo DataServiceKey serve ad indicare la (o
le) proprietà che definiscono la chiave dell'entità. Definiamo una
classe OfficeService in questo modo:
public class OfficeService
{
public IQueryable<Employee> Employees { get { return _employees.AsQueryable(); } }
public IQueryable<Department> Departments { get { return _departments.AsQueryable(); } }
private List<Employee> _employees = null;
private List<Department> _departments = null;
public OfficeService()
{
this._employees = new List<Employee>();
this._departments = new List<Department>();
Employee employee1 = new Employee()
{
Id = 1,
Name = "Giulio",
Surnamte = "Rossi",
Role = "Administrator"
};
Employee employee2 = new Employee()
{
Id = 2,
Name = "Mario",
Surnamte = "Verdi",
Role = "Administrator"
};
Employee employee3 = new Employee()
{
Id = 3,
Name = "Giuseppe",
Surnamte = "Bianchi",
Role = "Power User"
};
Department department1 = new Department() { Name = "Administration" };
department1.Employees.Add(employee1);
department1.Employees.Add(employee2);
employee1.Departament = department1;
employee2.Departament = department1;
Department department2 = new Department() { Name = "Front Office" };
department2.Employees.Add(employee3);
employee3.Departament = department2;
this._employees.Add(employee1);
this._employees.Add(employee2);
this._employees.Add(employee3);
this._departments.Add(department1);
this._departments.Add(department2);
}
}
Modifichiamo il codice del servizio:
public class DomusDotNetService : DataService<OfficeService>
{
public static void InitializeService(DataServiceConfiguration config)
{
config.SetEntitySetAccessRule("Employees", EntitySetRights.AllRead);
config.SetEntitySetAccessRule("Departments", EntitySetRights.AllRead);
//config.SetServiceOperationAccessRule("Employees", ServiceOperationRights.All);
config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
}
}
Dove T è OfficeService, ovvero il contenitore di
entità del modello di dati che dispone di una o più proprietà che
restituiscono un oggetto IQueryable, utilizzato per
accedere ai set di entità nel modello di dati. I comportamenti del
servizio dati vengono definiti dai membri della classe
DataServiceConfiguration, fornita al metodo
InitializeService implementato dal servizio dati. La
classe DataServiceConfiguration consente di specificare i
comportamenti del servizio ed in particolare i diritti di accesso
all'entità ed alle operazioni esposte dallo stesso. In particolare
utilizziamo l'enum EntitySetRights per configurare
l'accesso completo in lettura alle entità Employees
e Departments. Altre possibili configurazioni sono:
- None, nega tutti i diritti di accesso ai dati
- ReadSingle, autorizzazione all'esecuzione di una
singola query sull'entity set
- ReadMultiple, autorizzazione all'esecuzione di query
multiple sull'entity set
- WriteAppend, autorizzazione ad aggiungere nuove
entità all'entity set
- WriteReplace, fornisce l'autorizzazione ad eseguire
aggiornamenti basati su sostituzione. Il payload dell'aggiornamento
dovrebbe contenere tutte le proprietà dell'entità da sostituire.
Nel caso una o più proprietà da sostituire non siano presenti il
valore presente sul server verrà preservato
- WriteDelete, autorizza la cancellazione di data items
dall'entity set
- WriteMerge, autorizzazione l'esecuzione di
aggiornamenti basati sul merge dei dati. A differenza di
WriteReplace, nel payload dovrebbero essere specificate solo
le proprietà da aggiornare. Per le proprietà non specificate verrà
preso in considerazione il valore presente sul server
- AllRead, assegna tutti i diritti per la lettura dei
data items
- AllWrite, assegna tutti i diritti per la scrittura dei
data items
- All, assegna tutti diritti per la lettura\scrittura
dei data items
Possiamo eventualmente applicare le stesse
autorizzazioni a tutte le entità specificando come primo
parametro del metodo SetEntitySetAccessRule il carattere
*. Per impedire totalmente l'accesso ad un'entità è
sufficiente non configurarla o specificare il valore
EntitySetRights.none . Per il tipo di configurazione
impostata, eseguendo il codice, la pagina iniziale del browser
dovrebbe presentare un documento XML simile al seguente:

Definiamo nel nostro servizio un'operazione:
[WebGet]
public int GetEmployeesCount()
{
return (this.CurrentDataSource as OfficeService).Employees.Count();
}
Che restituisce il numero di Employee presenti nell'insieme
dati. Per rendere visibile il nostro metodo a chi consumerà i dati
del servizio, aggiungiamo alla configurazione questa linea di
codice:
config.SetServiceOperationAccessRule("GetEmployeesCount",
ServiceOperationRights.All);
Come per le entità abbiamo configurato i diritti di
accesso all'operazione GetEmployeesCount. Nello specifico,
possibili valori dell'enum ServiceOperationRights
sono:
- None, nessuna autorizzazione di accesso
all'operazione
- ReadSingle, autorizzazione a leggere un singolo data
item dell'entity set tramite l'operazione
- ReadMultiple, autorizzazione a leggere data items
multipli
- AllRead, autorizzazione in lettura singola o multipla
di data items
- All, assegna tutti i diritti all'operazione
- OverrideEntitySetRights, esegue l'override
dell'insieme dei diritti che sono esplicitamente definiti nel Data
Service con i diritti dell'operazione
Se digitiamo nella barra degli indirizzi del browser
l'indirizzo
http://localhost:3857/DomusDotNetService.svc/GetEmployeesCount
, otteniamo:

Dove "3" è uguale al numero di elementi presenti in
Employees. Essendo questo un articolo introduttivo,
approfondiremo con maggior dettaglio il discorso su configurazione,
entità ed operazioni nel prossimo articolo.
Ora che abbiamo creato il nostro primo servizio, vediamo quali
sono le modalità per eseguire delle query che ci permettano di
filtrare o mettere in join i dati. Negli esempi precedenti abbiamo
visto che la creazione di una query (semplice o complicata che sia)
si riduce alla creazione di un appropriato URI. Ad esempio per
recuperare la lista di tutti gli Employees o
Departments è sufficiente digitare nel browser un
indirizzo simile al seguente:
http://localhost:3857/DomusDotNetService.svc/Employees , ottenendo
qualcosa di questo tipo:

Per ottenere ad esempio l'Employee con ID=2 ,sarebbe
sufficiente digitare l'indirizzo
http://localhost:3857/DomusDotNetService.svc/Employees(2), mentre
per visualizzare l'istanza di Department associato all'Employee con
ID=1 potremmo digitare l'URI seguente
http://localhost:3857/DomusDotNetService.svc/Employees(1)/Departament.
Nei servizi OData, gli URI sono composti da tre parti:
- Service Root
- Resource Path
- Opzioni della query
Nell'esempio
http://localhost:3857/DomusDotNetService.svc/Employees(2),
http://localhost:3857/DomusDotNetService.svc è il Service
Root, /Employees il Resource Path e (2)
l'opzione della query. Per recuperare un sottoinsieme di
dati dell'entità specifica in base ad un filtro è possibile
utilizzare la keyword $filter come opzione della query
nella Resource Path . Ad esempio la query
http://localhost:3857/DomusDotNetService.svc/Employees?$filter=Name
eq 'Giulio' restituisce l'insieme di Employee che hanno la
proprietà Surname uguale (eq = equals) alla stringa Giulio:

L'elenco degli operatori applicabili in combinazione con $filter
sono:
- eq, uguale a
- ne, diverso da
- gt, più grande di
- ge, più grande o uguale a
- lt, minore di
- le, minore o uguale a,
- and, operatore logico AND
- or, operatore logico OR
- Not, operatore logico NOT
- Add, operazione di addizione
- Sub, operazione di sottrazione
- Mul, operazione di moltiplicazione
- Div, operazione di divisione
- Mod, operazione modulo
- ( ), precedenza di aggregazione
Oltre agli operatori, per interrogare i dati possiamo utilizzare
le seguenti funzioni booleane applicabili a stringhe:
- substringof(string p0, string p1), ritorna Vero se p0
è contenuta in p1,Falso altrimenti
- endswith(string p0, string p1), ritorna Vero se p0
finisce con p1, Falso altrimenti
- startswith(string p0, string p1), ritorna Vero se p0
inizia con p1, Falso altrimenti
- length(string p0), ritorna la lunghezza di p0
- indexof(string p0, string p1), ritorna la posizione
del primo carattere di p1 di p0
- replace(string p0, string pFind,string pReplace),
sostituisce la stringa pFind in p0 con pReplace e ritorna la
stringa così ottenuta
- substring(string p0, int pos), ritorna la sottostringa
di p0 che inizia alla posizione indicata da pos
- substring(string p0, int pos, int length), ritorna la
sottostringa di p0 che inizia alla posizione pos e di lunghezza
length
- tolower(string p0), converte la stringa p0 in
lowercase e ritorna la stringa ottenuta
- toupper(string p0), converte la stringa p0 in
uppercase e ritorna la stringa ottenuta
- trim(string p0), esegue il Trim della stringa p0 e
restituisce la stringa ottenuta
- concat(string p0, string p1), concatena le stringhe p0
e p1 e ritorna la stringa ottenuta
Esempi di query che utilizzano queste funzioni sono:
http://localhost:3857/DomusDotNetService.svc/Employees?$filter=startswith(Name,'Giu')
e
http://localhost:3857/DomusDotNetService.svc/Employees?$filter=substringof('iu',Name)
Ovviamente non mancano le funzioni per le date (tutte le funzioni
ritornano un intero):
- day(DateTime p0), ritorna il giorno del mese della
data in p0
- hour(DateTime p0), ritornta l'ora del giorno della
data in da p0
- minute(DateTime p0), ritorna il minuto dell'ora della
data in p0
- month(DateTime p0), ritorna il mese dell'anno della
data in p0
- second(DateTime p0), ritorna il secondi per il minuto
della data in da p0
- year(DateTime p0), ritorna l'anno della data in
p0
e per l'esecuzione di operazioni matematiche:
- round(double p0)
- round(decimal p0)
Le ultime due funzioni ritornano rispettivamente un double
ed un decimal, ed arrotondano il valore di p0 al numero intero più
vicino. Per i numeri positivi ritorneranno il primo numero intero
positivo più grande, per i numeri negativi il numero intero più
piccolo. Per le funzioni;
- floor(double p0)
- floor(decimal p0)
I valori di ritorno sono rispettivamente un double ed un decimal
e ritornano l'intero più grande minore o uguale alla valore p0.
- ceiling(double p0)
- ceiling(decimal p0)
In quest'ultimo caso le funzioni ritornano l'intero più piccolo
minore o uguale al valore del parametro di ingresso p0. A questo
punto, in un'applicazione reale, come creare le URI per l'accesso
ai dati ? Come richiamare le operazioni esposte dal servizio ? Per
ritornare i dati in formati diversi da XML, ad esempio JSON ? LINQ
? WCF Data Services nel futuro ? Cercheremo di rispondere ad alcune
di queste domande nella seconda parte dell'articolo.
Tags: xml,WCF 4,JSON,Atom,OData,Rest