Le Service Operations negli ADO.NET Data Services con Visual Basic 2008
a cura di Alessandro Del Sole (requisiti: conoscenze di base su ADO.NET Data Services)Introduzione
In questo articolo riprenderemo il discorso relativo agli ADO.NET Data Services con Visual Basic 2008, in particolare impareremo cosa sono e come si utilizzano le cosiddette Service Operations.
Il prerequisito necessario per proseguire nella lettura di questa trattazione, è aver letto almeno il primo dei miei articoli introduttivi ai Data Services in ambito Visual Basic 2008.Il codice sorgente a corredo del presente articolo è disponibile in area Download di Visual Basic Tips & Tricks e riprenderà lo stesso progetto con il quale abbiamo implementato il servizio WCF nel primo articolo, per poi utilizzare una diversa tipologia di client attraverso l'utilizzo di un'applicazione Windows Forms.
Questo approccio ha due finalità: illustrare l'utilizzo di un client Windows diverso da WPF (è una situazione non infrequente) e ci permetterà di ottenere il risultato desiderato più velocemente, permettendoci di concentrarci più sui concetti che sull'esteriorità.Riprendiamo il servizio
Iniziamo riprendendo il codice di partenza del nostro servizio, che espone in rete i dati del database dimostrativo Northwind. Il codice, molto semplice, è il seguente:Imports System.Data.Services Imports System.Linq Imports System.ServiceModel.Web Public Class NorthwindService ' TODO: replace [[class name]] with your data class name Inherits DataService(Of NORTHWNDEntities) ' This method is called only once to initialize service-wide policies. Public Shared Sub InitializeService(ByVal config As IDataServiceConfiguration) ' TODO: set rules to indicate which entity sets and service operations are visible, updatable, etc. ' Examples: config.SetEntitySetAccessRule("*", EntitySetRights.All) 'config.SetServiceOperationAccessRule("MyServiceOperation", ServiceOperationRights.All) End Sub End ClassVi rimando al primo articolo per i dettagli sulla creazione di ADO.NET Data Services e sull'implementazione del codice sopra esposto, mentre qui daremo spazio alle novità di nostro interesse.
Innanzitutto, modifichiamo la riga:config.SetEntitySetAccessRule("*", EntitySetRights.All)con le seguenti:
config.SetEntitySetAccessRule("Customers", EntitySetRights.AllRead) config.SetEntitySetAccessRule("Orders", EntitySetRights.All)In questo modo i client potranno accedere senza restrizioni agli ordini (potendo modificarli), ma solo in lettura ai dati relativi ai clienti.
Ora ricordiamo parte di quanto affermato in precedenza, ossia che, attraverso il metodo SetEntitySetAccessRule, per fini esclusivamente didattici erano stati assegnati permessi completi di lettura e scrittura su tutte le entità, specificando che questa non è una buona pratica negli scenari reali. In sostanza, è bene utilizzare SetEntitySetAccessRule impostando permessi specifici per ciascuna entità. Ad ulteriore suffragio di questa considerazione, il team di sviluppo degli ADO.NET Data Services ha previsto una importante caratteristica: le service operations. Vediamo insieme di cosa si tratta.
Cosa sono le Service Operations
Le Service Operations sono delle estensioni per Windows Communication Foundation specifiche per i Data Services che consentono di eseguire operazioni sui dati lato server, grazie alle quali lo sviluppatore stabilisce a priori le regole per l'accesso e può implementare la logica di business in un'ottica personalizzata. Per esempio, potrebbe essere necessario implementare delle regole di validazione personalizzate oppure far sì che l'accesso ai dati sia limitato ad operazioni di lettura.In altre parole, le service operations sono dei metodi .NET classici che vengono eseguiti, lato server, nei confronti del servizio e restituiscono tipicamente (ma non obbligatoriamente, come vedremo successivamente) un oggetto di tipo IQueryable(Of T), dove T è un'entità tra quelle mappate nell'Entity Data Model. Tali metodi eseguono interrogazioni sui dati sempre attraverso il protocollo Http e possono essere utilizzati sia in query string dal browser che fruiti dai client.
Come funzionano le service operations? Si tratta di metodi decorati con l'attributo WebGet per operazioni di lettura o WebInvoke per operazioni di modifica, che eseguono una specifica attività sulla fonte dati tramite LINQ e ne restituiscono il risultato sotto forma di:
- IQueryable(Of T), scenario prevalente nell'ambito di applicazioni data centric;
- IEnumerable(Of T), laddove non si voglia consentire al client operazioni tipiche offerte dalla IQueryable;
- uno dei tipi di dato primitivi in .NET Framework. Questo perché gli ADO.NET Data Services possono esporre dati provenienti anche da oggetti in memoria e non solo dall'Entity Framework;
- nessun tipo (quindi una Sub).
Per esempio, possiamo ipotizzare di voler implementare un metodo che restituisca tutti gli ordini di un determinato cliente, dato l'identificativo del cliente stesso. Ciò premesso, all'interno della classe sopra esposta digitiamo quanto segue:
<WebGet()> _ Public Function GetOrdersByCustomerId(ByVal CustomerId As String) As IQueryable(Of Orders) If String.IsNullOrEmpty(CustomerId) = True Then 'Id nullo Throw New ArgumentNullException("Nessun ID cliente specificato") Else 'CurrentDataSource rappresenta l'istanza dell'ObjectContext Return Me.CurrentDataSource.Orders.Where(Function(cust) cust.Customers.CustomerID = _ CustomerId).OrderBy(Function(ord) ord.OrderDate) End If End FunctionCome vedete, si tratta di un normalissimo metodo .NET, con la particolarità di eseguire un'interrogazione il cui risultato sarà restituito sotto forma di IQueryable. Inoltre, l'operazione viene interpretata da Windows Communication Foundation come HTTP GET, cioè come recupero di dati, situazione resa possibile dall'utilizzo dell'attributo WebGet.
Di fondamentale importanza in questa sede è l'utilizzo di un oggetto chiamato CurrentDataSource, che rappresenta l'istanza attiva dell'ObjectContext. D'altra parte non abbiamo fatto, lato server, alcuna dichiarazione di tipo Dim NorthwindContext As New NorthwindEntities come invece nei client, quindi CurrentDataSource (che nel nostro esempio è di tipo NORTHWNDEntities, definito nell'Entity Data Model, che eredita da ObjectContext) serve proprio a restituire l'istanza attiva del contesto.
Nello specifico, osserviamo l'utilizzo dei metodi extension Where e OrderBy, i quali ricevono entrambi delle espressioni lambda: la prima ci permette di determinare la corrispondenza tra id cliente ricevuto dal metodo e ciascun elemento della sequenza, la seconda ci occorre per stabilire la condizione di ordinamento dei dati. Se non vi piace l'utilizzo dei metodi extension, è possibile utilizzare una normale query LINQ (provider LINQ to ADO.NET Data Services, che abbiamo visto nel secondo articolo della serie).Riveste particolare importanza il fatto che le service operations consentano la restituzione, come risultato, di una query LINQ piuttosto che un'altra fonte dati. Infatti, ciò non è un caso ma un'importante accortezza avuta dal team di sviluppo. In questo modo, infatti, i client potranno elaborare la query attraverso operazioni di filtro, paginazione, ordinamento, selezione e presentare i dati elaborati all'utente secondo le proprie necessità.
Un esempio d'uso dell'attributo WebInvoke, del quale non faremo però utilizzo nella nostra applicazione di esempio, può essere il seguente, nel quale viene aggiunto un ordine all'entità Orders solo qualora la città di consegna sia Roma:
<WebInvoke()> _ Public Function AddOrder(ByVal order As Orders) As Boolean If order.ShipCity = "Rome" Then Me.CurrentDataSource.AddToOrders(order) Me.CurrentDataSource.AttachTo("Customers", order.Customers) Me.CurrentDataSource.SaveChanges() Return True Else Return False End If End FunctionVisibilità delle Service Operations
La sola implementazione della service operation, però, non è sufficiente a renderla fruibile dall'esterno. Dobbiamo infatti assegnarle esplicitamente dei permessi. A tal proposito rispolveriamo un metodo chiamato SetServiceOperationAccessRule che Visual Studio aveva aggiunto già al momento della creazione del servizio, ma reso inattivo dai commenti. Ciò premesso, decommentiamo la seguente riga di codice:'config.SetServiceOperationAccessRule("MyServiceOperation", ServiceOperationRights.All)e modifichiamola come segue:
config.SetServiceOperationAccessRule("GetOrdersByCustomerId", ServiceOperationRights.AllRead)In sostanza, sto assegnando al mio metodo permessi di sola lettura, perché il metodo stesso si limita a interrogare i dati così come sono, senza eseguire modifiche. Con un approccio di questo tipo, posso impedire ai client di avere determinati permessi sulle entità, restringendoli mediante SetEntitySetAccessRule, per poi consentire loro l'utilizzo di service operations per l'esecuzione di operazioni lato server in sola lettura come nell'esempio proposto.
Fruizione delle Service Operations
Le service operations possono essere fruite sia lato server che lato client, in entrambi i casi in query string. Tipicamente le service operations sono progettate per essere fruite dai client, in ogni caso proviamo ad avviare il nostro servizio da Visual Studio 2008 premendo F5. Nel momento in cui il servizio mostra i dati nel browser, possiamo digitare la seguente riga nella barra degli indirizzi:http://LocalHost:1566/NorthwindService.svc/GetOrdersByCustomerId?CustomerId='ALFKI'Così facendo, nel browser verrà mostrato l'elenco degli ordini relativi al cliente ALFKI secondo il consueto approccio REST:
![]()
Ora aggiungiamo alla nostra soluzione un nuovo progetto Windows Forms. Tramite l'interfaccia, consentiremo ai nostri ipotetici utenti di inserire un identificativo cliente per poi interrogare il servizio e mostrare l'elenco degli ordini riferibili al cliente specificato. L'interfaccia del form principale dovrà risultare come in figura:
![]()
Abbiamo quindi necessità dei seguenti controlli:
- un BindingSource (chiamato OrdersBindingSource);
- un BindingNavigator (chiamato OrdersBindingNavigator);
- una DataGridView (chiamata OrdersDataGridView);
Aggiungiamo infine una casella di testo e un pulsante, come illustrato in figura, sul BindingNavigator. L'interfaccia è in sé molto essenziale, ma a noi serve solo capire come utilizzare le Service Operations e vedere come utilizzare le applicazioni Windows Forms come client.
Ora bisogna aggiungere un riferimento al servizio, come facemmo anche la volta scorsa. Selezioniamo il comando Project|Add service reference e vedremo comparire la seguente finestra, che ormai dovremmo conoscere:
![]()
Facendo clic sul pulsante Discover, verrà automaticamente raggiunto il servizio contenuto nella soluzione. Modifichiamo l'identificatore per il namespace come indicato nella figura e facciamo clic su OK. Questa serie di passaggi dovrebbe comunque esservi familiare se avete letto il secondo articolo della serie.
Passiamo quindi al codice Visual Basic, per rendere operativa la nostra applicazione client. In primo luogo ci occorre la seguente direttiva Imports, per abbreviare il richiamo agli oggetti esposti dal servizio:
Imports ServiceOperationsClient.NorthwindServiceReferenceAnalogamente a quanto visto la volta scorsa, il contesto viene istanziato passando l'indirizzo del servizio:
Public Sub New() ' This call is required by the Windows Form Designer. InitializeComponent() ' Add any initialization after the InitializeComponent() call. Me.NorthwindContext = New NORTHWNDEntities(New Uri("Http://localhost:1566/NorthwindService.svc")) End SubOra gestiamo l'evento Click del pulsante sul BindingNavigator, all'interno del quale scriveremo il codice per richiamare la service operation implementata nel servizio:
Private Sub ToolStripButton1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles ToolStripButton1.Click If String.IsNullOrEmpty(Me.ToolStripTextBox1.Text) = False Then Dim queryString As String = "GetOrdersByCustomerId?CustomerId='" _ + Me.ToolStripTextBox1.Text + "'" Me.OrdersBindingSource.DataSource = Me.NorthwindContext.Execute(Of Orders)( _ New Uri(queryString, UriKind.RelativeOrAbsolute)) Me.OrdersDataGridView.AutoGenerateColumns = True Me.OrdersDataGridView.DataSource = Me.OrdersBindingSource End If End SubCome vedete, abbiamo costruito una stringa per l'esecuzione della query string, contenente il nome della service operation completata dall'identificativo del cliente. Le service operation vengono richiamate tramite il metodo Execute(Of T) del contesto, il cui argomento è un Uri costituito dalla query string stessa. Execute serve, in linea più generale, ad eseguire delle query string e, poiché le service operations sono richiamate proprio tramite queste stringhe (come abbiamo visto nel servizio), si fa ricorso apposta a tale metodo. Chiaramente, abbiamo utilizzato Execute(Of Orders) poichè Orders è l'entità di partenza del risultato IQueryable restituito dal servizio.
Se ora proviamo ad eseguire l'applicazione, possiamo digitare l'identificativo di un cliente, fare clic sul pulsante di ricerca ordini e vedere cos'accade:![]()
Grazie alla service operation, abbiamo ottenuto l'elenco degli ordini effettuati dal cliente ALFKI. Forse con una visione 'grafica' del risultato è più facile capire l'utilità dell'aver ottenuto un tipo IQueryable(Of T): potremmo prevedere operazioni di paginazione, ordinamento e filtro sul risultato della query per poi esporlo attraverso l'interfaccia.
Conclusioni
In questo articolo abbiamo visto come le service operations permettano allo sviluppatore di gestire la business logic all'interno degli ADO.NET Data Services, rendendo questa tecnologia estremamente flessibile nel gestire le regole di accesso ai dati. Abbiamo anche visto come utilizzare un'altra tipologia di applicazioni .NET come client e questo è dovuto proprio all'infrastruttura così versatile di .NET Framework.
Per commenti e quant'altro di vostro interesse, potete contattarmi al mio indirizzo visitare il mio blog.