WCF Data Services - Terza parte

Scritto da  Pietro Libro il mercoledì 25 gennaio 2012
Linguaggio: C#   •  Framework: 3.5,4.0   •  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:

Figura_1

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:

Figura_2

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:

Figura_3

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:

  1. Anonymous authentication, ovvero nessuna, se vogliamo rendere un particolare servizio accessibile a tutti
  2. 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
  3. Windows authentication, basata su NTLM o Kerberos, ovviamente è necessario che il client sia un'applicazione basata su Windows
  4. 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.
  5. Claims-based authentication, dove l'autenticazione del client è affidata ad una terza parte che rilascia un token che garantisce l'accesso alle risorse richieste.
  6. 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:

Figura_4

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

Figura_5

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

Figura_6

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

 
x