WCF Data Services - Prima Parte

Scritto da  Pietro Libro il mercoledì 21 dicembre 2011 (aggiornato il 21 dicembre 2011)
Linguaggio: C#   •  Framework: 3.5,4.0   •  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.

Architettura_WCF_Data_Service

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:

Figura_1

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

Figura_2

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):

Figura_3

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:

Figura_4

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:

Figura_5

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:

Figura_6

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:

Figura_7

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:

  1. Service Root
  2. Resource Path
  3. 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:

Figura_8

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

 
x