WCF Data Services - Terza parte
Scritto da
Pietro Libro
il
mercoledì 25 gennaio 2012
Linguaggio:
•
Framework:
•
Livello: 200
In questo terzo ed ultimo appuntamento dedicato alla serie,
arricchiremo il codice degli esempi visti nella prima e
seconda parte aggiungendo funzionalità di caching e
secuirity delle risorse esposte dal nostro servizio.
Come visto precedentemente, i WCF Data Services sono costruiti al
di sopra dell'architettura ASP.NET, il che ci permette di
utilizzarne tutte le vare features, tra le quali
l'output caching, per eseguire il caching dei servizi.
Normalmente, sfruttando il metodo GET dell'HTTP, sempre che
l'output caching sia abilitato, ASP.NET esegue un caching
lato server delle chiamate utilizzando l'indirizzo passato per la
richiesta di una pagina. La "chiave" utilizzata è ovviamente l'URI
passato al server: il matching è l'indirizzo che deve
coincidere con quella in memoria (nel caso dei WCF Data Service
questo deve comprendere oltre all'indirizzo base anche le eventuali
query options). Aggiungere questa funzionalità al nostro
esempio è relativamente semplice, è sufficiente aggiungere qualche
riga di codice, ma prima di vederne il funzionamento, aggiungiamo
qualche Employee alla nostra base dati utilizzando
le UI della nostra applicazione ASP.NET MVC di esempio, al fine di
ottenere una lista simile alla seguente:

Per verificare cosa succede dietro le quinte, apriamo (da SQL
Server Management Studio) un'istanza del SQL Server Profiler e
proviamo ad aggiornare continuamente la pagina (ad esempio da
tastiera tramite F5); è facile vedere come ad ogni refresh
corrisponda una chiamata al server:

Questo potrebbe essere un comportamento non desiderato, in
quanto caricare continuamente dati (immutati in questo caso) è
un'inutile spreco di risorse in termini di CPU, memoria,
infrastruttura ecc... Per ovviare ed eseguire il caching di pagine
di questo tipo possiamo eseguire l'override del metodo
OnStartProcessingRequest della nostra classe
DomusDotNetService (questo metodo viene invocato ad ogni
richiesta di elaborazione) in questo modo:
protected override void OnStartProcessingRequest(ProcessRequestArgs args)
{
HttpContext context = HttpContext.Current;
HttpCachePolicy cachePolicy = HttpContext.Current.Response.Cache;
cachePolicy.SetCacheability(HttpCacheability.ServerAndPrivate);
cachePolicy.SetExpires(DateTime.MaxValue);
cachePolicy.VaryByHeaders["Accept"] = true;
cachePolicy.VaryByHeaders["Accept-Charset"] = true;
cachePolicy.VaryByHeaders["Accept-Encoding"] = true;
cachePolicy.VaryByParams["*"] = true;
cachePolicy.SetValidUntilExpires(true);
}
A questo punto, la prima volta che carichiamo la pagina verrà
eseguita una query sul database (come mostrato nell'immagine
seguente), successivamente (utilizzando sempre il nostro SQL Server
Profiler) , aggiornando la pagina, anche continuamente, possiamo
vedere come non verranno più effettuate chiamate lato server:

ASP.NET eseguirà il caching per ogni Entity Set e per
ogni variazione di parametro. Nel codice di esempio abbiamo
impostato una cache praticamente eterna, in quanto, tramite il
metodo SetExpires(DateTime.MaxValue) abbiamo
impostato la data di scadenza al 31/12/9999. Questo
comportamento non è desiderato in quanto aggiungendo un nuovo
Employee e tornando indietro alla lista degli
Employees registrati, la nuova voce non verrebbe
visualizzata. A tal fine, potremmo optare per eseguire il
caching dei dati per brevi periodi, o meglio eseguire
l'invalidazione della cache all'aggiornamento dei dati.
Per come sono strutturati i WCF Data Services, tutto ciò che
non è GET, allora è PUT, MERGE o DELETE, di conseguenza potremmo
sfruttare questa assunzione per eseguire un'invalidazione
automatica della cache, insieme al concetto di
CacheItemDependency di ASP.NET
(http://msdn.microsoft.com/en-us/library/ms178604%28v=vs.100%29.aspx)
:
HttpContext context = HttpContext.Current;
if (context.Request.HttpMethod.Equals("POST", StringComparison.OrdinalIgnoreCase) ||
context.Request.HttpMethod.Equals("MERGE", StringComparison.OrdinalIgnoreCase) ||
context.Request.HttpMethod.Equals("PUT", StringComparison.OrdinalIgnoreCase) ||
context.Request.HttpMethod.Equals("DELETE", StringComparison.OrdinalIgnoreCase))
{
context.Cache.Remove(_cacheKey);
}
HttpCachePolicy cachePolicy = HttpContext.Current.Response.Cache;
cachePolicy.SetCacheability(HttpCacheability.ServerAndPrivate);
cachePolicy.SetExpires(DateTime.MaxValue);
cachePolicy.VaryByHeaders["Accept"] = true;
cachePolicy.VaryByHeaders["Accept-Charset"] = true;
cachePolicy.VaryByHeaders["Accept-Encoding"] = true;
cachePolicy.VaryByParams["*"] = true;
cachePolicy.SetValidUntilExpires(true);
context.Response.AddCacheItemDependency(_cacheKey);
if (context.Cache.Get(_cacheKey) == null)
{
context.Cache.Insert(_cacheKey, "dummy");
}
Il comportamento potrebbe essere quello desiderato, ma il
controllo della cache è a "grana grossa". Per migliorare possiamo
utilizzare le query interceptors che ci permettono di
avere accesso alle richieste sulle varie entità e di
conseguenza aggiungere della logica custom per onguna di essa. Per
intercettare una richiesta possiamo decorare un metodo del
nostro Data Service con l'attributo
QueryInterceptorAttribute o
ChangeInterceptorAttribute specificando l'Entity
Set che vogliamo intercettare.
Ad esempio per eseguire l'output caching delle query
sull'Entity Set degli Employees, potremmo
scrivere qualcosa del tipo:
[QueryInterceptor("Employees")]
public Expression<Func<Employee, bool>> OnQueryEmployees()
{
HttpContext context = HttpContext.Current;
HttpCachePolicy cachePolicy = HttpContext.Current.Response.Cache;
cachePolicy.SetCacheability(HttpCacheability.ServerAndPrivate);
cachePolicy.SetExpires(DateTime.MaxValue);
cachePolicy.VaryByHeaders["Accept"] = true;
cachePolicy.VaryByHeaders["Accept-Charset"] = true;
cachePolicy.VaryByHeaders["Accept-Encoding"] = true;
cachePolicy.VaryByParams["*"] = true;
cachePolicy.SetValidUntilExpires(true);
context.Response.AddCacheItemDependency(_cacheKey);
if (context.Cache.Get(_cacheKey) == null)
{
context.Cache.Insert(_cacheKey, "dummy");
}
return p => true;
}
E poi utilizzare un metodo decorato con
OnChangeEmployees per intercettare gli aggiornamenti ed
eseguire, eventualmente logica di validazione o l'applicazione di
vincoli prima che i dati siano aggiornati.
[ChangeInterceptor("Employees")]
public void OnChangeEmployees(Employee employee, UpdateOperations operations)
{
…
}
Nel caso specificato questo non sarebbe sufficiente in quanto
per la cancellazione utilizziamo la Service Operation
"DeleteEmployee" che dovremmo modificare in questo modo:
[WebGet]
public bool DeleteEmployee(int employeeId)
{
Boolean deleteCompleted = false;
ObjectContext objContext = (this.CurrentDataSource as ObjectContext);
Employee employeeToDelete = objContext.CreateObjectSet<Employee>()
.Where(e => e.Id == employeeId).FirstOrDefault();
if (employeeToDelete != null)
{
objContext.DeleteObject(employeeToDelete);
deleteCompleted = (objContext.SaveChanges() >= 1);
if (deleteCompleted)
{
HttpContext context = HttpContext.Current;
context.Cache.Remove(_cacheKey);
}
return deleteCompleted;
}
return false;
}
Come prima, attraverso il "solito" SQL Profiler possiamo
verificare il funzionamento del nostro sistema di caching. Le
possibilità di caching non finiscono ovviamente qui, dato che lato
ASP.NET possiamo utilizzare un custom OutputCacheProvider
, oppure altri sistemi\tecnologie come l'Enterprise Library
scaricabile da CodePlex all'indirizzo http://entlib.codeplex.com/
ed a cui dedicheremo presto uno o più articoli.
Come è facile intuire, le query interceptors possono
essere facilmente utilizzate per applicare politiche di sicurezza
ai nostri servizi. Per quanto concerne questo aspetto abbiamo già
avuto modo di parlarne implicitamente quando abbiamo configurato
l'accesso in lettura e scrittura alle risorse esposte dal nostro
servizio DomustDotNetService, ma per mettere al sicuro i nostri WCF
Data Services abbiamo bisogno di mettere in piedi anche un sistema
di autenticazione e di autorizzazione, per identificare
univocamente il "richiedente" della richiesta e quindi verificare
che possa accedere al servizio, ed in seconda battuta
che abbia i requisiti sufficienti per accedere ad una risorsa
piuttosto che ad un'altra. Le varie opzioni di autenticazione che
possiamo utilizzare per i WCF Data Services sono:
- Anonymous authentication, ovvero nessuna, se vogliamo
rendere un particolare servizio accessibile a tutti
- Basic - Digest authentication, credenziali fornite da
una coppia username-password. In entrambi i casi (Basic) e Digest,
le informazioni sono inviate in chiaro, quindi bisognerebbe
adottare SSL per lo scambio di informazioni tra Client e Server.
Questo tipo di autenticazione può essere utilizzata anche per i
client non Windows
- Windows authentication, basata su NTLM o Kerberos,
ovviamente è necessario che il client sia un'applicazione basata su
Windows
- ASP.NET Forms authentication, una delle più conosciute
in ambito web che permette di mantenere un token di autenticazione
in un cookie o nella URL della pagina. Anche in questo dovrebbe
essere considerato l'utilizzo di SSL per proteggere le credenziali
di accesso ed il ticket di autenticazione.
- Claims-based authentication, dove l'autenticazione del
client è affidata ad una terza parte che rilascia un token che
garantisce l'accesso alle risorse richieste.
- WIF
Quale utilizzare ovviamente dipende dal contesto in cui ci trova.
Quello che dovrebbe essere chiaro è che OData (e i WCF Data
Services nello specifico) sono basati sul protocollo HTTP , e
quindi i messaggi inviati e\o ricevuti (secondo del tipo di
autenticazione scelta) possono contenere informazioni
sensibili sia a livello di credenziali che di informazioni
riservate, pertanto quando possibile, un livello di
protezione alto può essere dato dall'adozione di SSL per lo scambio
dati. Concentriamoci ora sulla parte di security: quando
abbiamo creato la nostra applicazione di esempio utilizzando
il template di ASP.NET MVC, abbiamo già integrato un meccanismo di
autenticazione tramite account utente e password,
sfruttiamolo per il nostro esempio:

Avviamo l'applicazione e clicchiamo sul tasto in alto a destra,
non avendo ancora un account provvediamo ad inserirne uno nuovo (il
classico "pinco.pallo" :-) se volete ) :

A questo punto proviamo ad accedere utilizzando il nostro
account, in assenza di problemi la nostra pagina dovrebbe essere
simile alla seguente:

A questo punto non ci resta che mettere mano al codice. Basta
modificare il codice di OnQueryEmployees in questo modo
per testarne il funzionamento:
[QueryInterceptor("Employees")]
public Expression<Func<Employee, bool>> OnQueryEmployees()
{
var user = HttpContext.Current.User;
if (user.Identity.IsAuthenticated)
return p => true;
else
{
return p => false;
}
}
In presenza di ruoli avremmo potuto utilizzare del codice
tipo:
if (user.IsInRole("Administrator"))
{
…
}
Per avere una maggior controllo sull'accesso alle risorse in
base al ruolo coperto dall'utente. Ovviamente non sono sufficienti
queste poche righe di codice per esaurire un discorso così
importante quale la sicurezza (ritorneremo quanto prima
sull'argomento :-) ). Per il caso ASP.NET MVC è possibile
autorizzare la chiamata alle singole View o all'intero controller
utilizzando uno dei costruttori dell'attributo
Authorize.
Si conclude a questo punto il nostro "viaggio" attraverso i WCF
Data Services, che attualmente presentano alcune limitazioni (ad
esempio il non supporto al DbContext e\o al caching diretto),
superate con le prossime versioni (è possibile scaricare la
versione CTP di ottobre 2011), nelle quali troveremo molte novità
come il supporto ai dati geo-spaziali, Data Caching
Extensions ecc... e su cui ritorneremo a discutere presto.
Tags: WCF,ADO.NET,JSON,xml,AtomPub,OData,Rest