Issuu on Google+

Capitolo 26 Gli XML Web service

1127

26 Gli XML Web service Come è stato detto nel capitolo 1, un XML Web service è una componente .NET che risponde a richieste HTTP formattate tramite la sintassi SOAP. Gli XML Web service rappresentano una delle pietre angolari dell’iniziativa .NET, nel senso che, grazie a Internet, consentono un grado di interoperabilità tra le applicazioni assolutamente inconcepibile fino a poco tempo fa. Data l’enorme importanza che gli XML Web service rivestono, qualcuno potrebbe sorprendersi nel vedere il relativo poco spazio che è stato loro dedicato in questo libro. La spiegazione, in realtà, è semplice: gli XML Web service sfruttano molte funzionalità del .NET Framework ampiamente descritte nei precedenti capitoli. Innanzitutto, dal momento che gli XML Web service non sono altro che applicazioni ASP.NET – o più precisamente, vengono implementati come gestori HTTP che intercettano richieste da file .asmx – è possibile applicare la maggior parte dei concetti descritti nel capitolo 24, tra cui la gestione dello stato, la memorizzazione nella cache dell’output e l’autenticazione. Come si potrà vedere, sarà possibile creare XML Web service ancora migliori mettendo in pratica le nozioni sulla serializzazione XML del capitolo 11 e quelle sulle operazioni asincrone del capitolo 13.

Nota

Per mantenere il codice il più compatto possibile, tutti gli esempi di questo capitolo assumono l’inserimento delle seguenti istruzioni Imports all’inizio dei file sorgente:

Imports Imports Imports Imports Imports Imports

System.Web.Services System.Web.Services.Protocols System.Net System.Threading System.Web.Security System.IO

Introduzione agli XML Web service Invece di illustrare per prima la teoria degli XML Web service, comincerò col mostrare come creare un semplice XML Web service e accedervi da un programma Visual Basic .NET. Come accade per qualsiasi applicazione .NET, è possibile realizzare un XML Web service utilizzando Notepad e gli strumenti a riga di comando forniti dal .NET Framework. Tuttavia, Visual Studio .NET rende tutto talmente semplice che è difficile resistere alla tentazione di utilizzarlo. Nei paragrafi che seguiranno,


1128

Parte VI Applicazioni Internet

mostrerò come creare un semplice XML Web service per eseguire la conversione da dollari in euro e viceversa. (Per semplificare il tutto, ho inserito il tasso di cambio direttamente nel codice).

Creare il progetto XML Web Service Si lancia Visual Studio e si crea un nuovo progetto ASP.NET Web Service denominato MoneyConverter, mostrato in figura 26-1. Visual Studio crea una sottodirectory della directory principale di IIS e le attribuisce lo stesso nome del progetto, proprio come accade per un progetto ASP.NET Web Form. Analogamente alle applicazioni Web Form, il nuovo progetto contiene i propri file web.config e global.asax. Inoltre include altri due file che non è possibile trovare nei progetti Web Form: MoneyConverter.vsdisco e Service1.asmx. Per il momento, sarà proprio quest’ultimo a essere preso in considerazione.

Figura 26-1. Creare un progetto ASP.NET Web Service.

Il file Service1.asmx contiene il codice dell’XML Web service il quale – ancora una volta – rappresenta una componente .NET alla quale si accede da Internet via HTTP. Prima di proseguire con l’esempio, sarebbe bene attribuire a questo file un nome più descrittivo, come Converter.asmx. Dal momento che in generale si accede a una pagina .asmx da codice, il fatto di rinominarlo non è così importante come nel caso di una pagina .aspx, anche se, in ogni caso, è bene mantenere i file ordinati. Come i file delle Web Form, anche i file .asmx sono dotati di un’area di progettazione sulla quale è possibile trascinare le componenti .NET. Dal momento che gli XML Web service non possiedono interfaccia utente, tale caratteristica verrà utilizzata solo per le componenti non visuali, come le connessioni di ADO.NET o gli oggetti FileSystemWatcher. Il passo successivo consiste nel premere F7 e passare all’editor di codice: come si può vedere, Visual Studio ha già creato una classe XML Web service funzionante denominata Service1, con un metodo Hello World d’esempio pronto per essere decommentato. A questo punto si rinomina la classe Converter per mantenerla conforme al nome del file e si aggiungono i due metodi per convertire le valute, inserendoli dopo la procedura d’esempio commentata. Quello che segue è il codice del primo XML Web service. Per risparmiare spazio, ho omesso il codice generato automaticamente nel blocco #Region: <WebService(Description:=”A web service for converting currencies”, Namespace:=”http://tempuri.org/”) > _ Public Class Converter Inherits System.Web.Services.WebService #Region “ Web Services Designer Generated Code “ § #End Region


Capitolo 26 Gli XML Web service

1129

<WebMethod(Description:=”Convert from Euro to Dollar currency”)> _ Function EuroToDollar(ByVal amount As Decimal) As Decimal Return amount * GetEuroToDollarConversionRate() End Function <WebMethod(Description:=”Convert from Dollar to Euro currency”)> _ Function DollarToEuro(ByVal amount As Decimal) As Decimal Return amount / GetEuroToDollarConversionRate() End Function Private Function GetEuroToDollarConversionRate() As Decimal ‘ Un’applicazione reale leggerebbe questo numero da un file o un ‘ database. Return 0.9@ End Function End Class

Il codice effettivo per i due metodi è talmente semplice che non vale la pena commentarlo. Piuttosto, è bene soffermarsi sugli attributi utilizzati nel listato precedente: ■ L’attributo WebService qualifica la classe Converter come una classe XML Web service. Questo attributo non è realmente indispensabile in quanto l’estensione .asmx e il fatto che la classe Converter derivi da System.Web.Services.WebService sono sufficienti a comunicare all’infrastruttura di ASP.NET l’intenzione di realizzare un XML Web service. Tuttavia, l’attributo WebService è utile per passare informazioni aggiuntive sull’XML Web service, come la relativa descrizione e il namespace, e in pratica non andrebbe mai omesso. ■ L’attributo WebMethod rende un metodo della classe accessibile tramite Internet. Solo i metodi contrassegnati con tale attributo risultano visibili da client remoti, pertanto non è consentito ometterli. È possibile utilizzare tale attributo per associare una descrizione al metodo e per definire altre proprietà importanti, come il tipo di gestione per lo stato della sessione e il caching. (Per ulteriori informazioni a riguardo, si rimanda al paragrafo “L’attributo WebMethod” più avanti in questo capitolo).

Collaudare l’XML Web Service nel browser Il modo più semplice per collaudare l’XML Web service appena creato consiste nell’eseguire il progetto, dopo aver verificato che Converter.asmx sia stata definita come pagina iniziale. Visual Studio lancerà, quindi, una nuova istanza di Microsoft Internet Explorer e farà in modo che punti alla pagina .asmx. La figura 26-2 mostra quanto viene visualizzato nel browser. Il titolo della pagina è il nome della classe, e la proprietà description dell’attributo WebService appare immediatamente al di sotto, seguita dall’elenco dei metodi disponibili e dalle rispettive descrizioni. Ma da dove provengono queste informazioni? Quando ASP.NET intercetta una richiesta per una pagina .asmx senza alcun parametro nella query string, utilizza la reflection per estrarre i nomi degli attributi e dei metodi della prima classe del file .asmx, e quindi sintetizza la pagina HTML. (Se il file contiene più classi che derivano da System.WebServices.WebService, solo la prima classe sarà visibile nel browser). In un’applicazione reale, i client accedono al servizio da codice, ma questa utile funzionalità consente di collaudare il servizio in modo interattivo durante la fase di debugging. La pagina che genera l’output in questione non è altro che una pagina .aspx. Più precisamente, è il file DefaultWsdlHelpGenerator.aspx memorizzato nella directory C:\WinNT\Microsoft.NET\Framework\ vx.y.zzzz\Config. Dal momento che si tratta di una pagina .aspx standard, è possibile personalizzarla a seconda delle necessità, aggiungendo, ad esempio, il logo della società o utilizzandola per nascondere informazioni sull’XML Web service. È anche possibile modificare alcune costanti definite in prossimità dell’inizio della pagina per abilitare la modalità debugging o altre funzionalità


1130

Parte VI Applicazioni Internet

Figura 26-2. La pagina HTML che ASP.NET crea al volo quando si accede a un file .asmx senza passare alcun parametro.

Il fatto di apportare modifiche al file DefaultWsdlHelpGenerator.aspx influisce su tutti gli XML Web service in esecuzione sul computer. Per un controllo più fine sulle modalità di utilizzo della pagina, è possibile aggiungere un tag <wsdlHelpGenerator> al file web.config nella directory radice dell’applicazione oppure in quella che contiene il file .asmx: <configuration> <system.web> <webServices> <wsdlHelpGenerator href=”LocalWsdlHelpGenerator.aspx” /> </webServices> </system.web> </configuration>

La pagina di help standard offre ben più di una semplice descrizione dell’XML Web service e dei relativi metodi: consente, infatti, di invocare in modo interattivo i singoli metodi passando loro tutti gli attributi necessari. Non tutti i metodi possono essere collaudati in questo modo – ad esempio non quelli che accettano oggetti o argomenti ByRef – ma è sicuramente un enorme vantaggio quando si collauda il servizio. La Figura 26-3 mostra la pagina che appare quando si seleziona il nome di un metodo (DollarToEuro, in questo esempio). Basta inserire un valore qualsiasi nella casella Amount e premere il pulsante Invoke. Il valore di ritorno del metodo verrà visualizzato sotto forma di XML all’interno di una nuova istanza di Internet Explorer (figura 26-4). L’URL utilizzato per ottenere il risultato ha il seguente formato: http://servername/webservicename/pagename.asmx/methodname?arg=value

Ogni client remoto che sa come cercare questo XML Web service e quali argomenti passare può invocare un remotamente metodo inviando una richiesta HTTP GET. Come sarà possibile apprendere in seguito, questo è solo uno dei tre protocolli utilizzabili per interrogare un XML Web service da Internet, gli altri due sono HTTP POST e SOAP.


Capitolo 26 Gli XML Web service

1131

Figura 26-3. È possibile invocare un metodo di un XML Web service all’interno del browser.

Figura 26-4. Il valore di ritorno del metodo di un XML Web service viene formattato come XML.

La pagina di help principale dell’XML Web service contiene un link che può rivelarsi interessante. Se si seleziona il collegamento Service Description, il browser visualizza il contratto Web Service Description Language (WSDL). Si tratta di un file XML che descrive l’XML Web service appena creato, con informazioni su ciascun metodo e sugli argomenti che questo si aspetta. È possibile esaminare il contratto WSDL di una pagina .asmx direttamente dal browser aggiungendo l’argomento ?WSDL all’URL della pagina .asmx. Sebbene gli XML Web service si basino pesantemente sui file WSDL, nella maggior parte dei casi non sarà necessario preoccuparsi né di questi né delle informazioni che contengono in quanto Visual Studio sarà in grado di gestire questi file in modo trasparente.

Creare un client per un XML Web service Una volta verificato che l’XML Web service MoneyConverter funziona correttamente all’interno di un browser, si può passare alla creazione di un’applicazione client che lo utilizzi. Ogni applicazione in grado di inviare una richiesta HTTP può fungere da client per l’XML Web service, e non è necessario che sia un’applicazione .NET o Windows. Se ci si limita a considerare esclusivamente le applicazioni gestite, un tipico client per un XML Web service può essere: un’applicazione Windows Form, un’applicazione Web Form oppure un’altra applicazione XML Web service. Nei paragrafi che seguiranno,


1132

Parte VI Applicazioni Internet

mostrerò come creare un client Windows Form, ma la procedura da seguire si applica anche alla realizzazione di altri tipi di client. 1. Si aggiunge un progetto Windows Forms alla soluzione corrente, gli si attribuisce il nome MoneyConverterClient, e lo si rende il progetto di start-up per la soluzione. 2. Si seleziona il comando Add Web Reference dal menu Project o dal menu di contesto che appare effettuando un clic con il tasto destro del mouse sul nodo del progetto nella finestra Solution Explorer. 3. La finestra di dialogo che appare consente di selezionare un XML Web service tra quelli registrati in una directory Universal Description, Discovery, and Integration (UDDI). (Figura 26-5). Le directory UDDI rappresentano una sorta di pagine gialle per gli XML Web service nel senso che elencano i servizi distribuiti in Internet. (Per ulteriori informazioni sugli UDDI è possibile visitare il sito http://www.uddi.org).

Figura 26-5. La finestra di dialogo Add Web Reference.

4.

Si immette il percorso del file .asmx sulla macchina locale – ossia, http://localhost/MoneyConverter/ converter.asmx – nel campo Address e si preme Invio. Se il percorso è corretto, il pannello di sinistra mostrerà la pagina di descrizione associata all’XML Web Service (Figura 26-6.) 5. Il pannello di destra contiene collegamenti aggiuntivi per visualizzare il contratto WSDL e la pagina di help predefinita. Dal momento che l’XML Web service funziona correttamente, è possibile proseguire e premere il pulsante Add Reference. Visual Studio aggiunge una nuova cartella Web References al progetto, con un nodo localhost che raccoglie tutti i file che ha creato o che ha scaricato dall’XML Web service: ad esempio, il file del contratto WSDL. Il file principale, dal punto di vista dell’applicazione client, è Reference.vb. (Per visualizzarlo è necessario premere il pulsante Show All Files all’interno del Solution Explorer ed espandere il nodo Web References, Localhost e Reference.map). Questo file contiene una classe Converter che espone gli stessi metodi degli XML Web service originali (e qualcuno in più) e che funge da proxy tra l’applicazione Windows Form e l’XML Web service in esecuzione da qualche parte su Internet (figura 26-7).


Capitolo 26 Gli XML Web service

1133

Figura 26-6. L’inserimento di un indirizzo di una pagina .asmx abilita il pulsante Add Reference.

Invece di inviare le richieste HTTP all’XML Web service, l’applicazione client invoca i metodi di questa classe proxy, e questa, a sua volta, reindirizza la chiamata all’XML Web service utilizzando il protocollo HTTP.

Figura 26-7. Come un riferimento Web appare nel Solution Explorer e nell’Object Browser.


1134

Parte VI Applicazioni Internet

Come spesso accade nel mondo .NET, questa piccola magia è possibile grazie all’ereditarietà. La classe proxy Converter eredita molti dei propri metodi dalla classe di base System.Web.Services. Protocol.SoapHttp ClientProtocol. Il più importante di tali metodi è Invoke, il quale effettua una sorta di chiamata in late binding alla classe XML Web service remota. La classe generata automaticamente da Visual Studio contiene procedure wrapper che consentono all’applicazione client di invocare metodi remoti utilizzando una sintassi fortemente tipizzata: ‘ ...(All’interno della classe Converter nel file Reference.vb)... Public Function EuroToDollar(ByVal amount As Decimal) As Decimal Dim results() As Object = Me.Invoke(“EuroToDollar”, New Object() {amount}) Return CType(results(0), Decimal) End Function

Una volta compreso il funzionamento di tale meccanismo, per invocare l’XML Web service basta creare un’istanza della classe proxy e invocare uno dei relativi metodi, senza doversi preoccupare di ciò che accade dietro le quinte. La figura 26-8 mostra una semplice form che consente di convertire i dollari in euro e viceversa. Quello che segue è il codice che gestisce i due pulsanti: Private Sub btnToEuros_Click(ByVal sender As Object, ByVal e As EventArgs) _ Handles btnToEuros.Click Dim conv As New localhost.Converter() ‘ Crea il proxy. Dim value As Decimal = CDec(txtDollars.Text) ‘ Ottiene l’argomento. Dim result As Decimal = conv.DollarToEuro(value) ‘ Invoca il metodo remoto. txtEuros.Text = result.ToString ‘ Visualizza il risultato. End Sub Private Sub btnToDollars_Click(ByVal sender As Object, Handles btnToDollars.Click Dim conv As New localhost.Converter() ‘ Dim value As Decimal = CDec(txtEuros.Text) ‘ Dim result As Decimal = conv.EuroToDollar(value) ‘ txtDollars.Text = result.ToString ‘ End Sub

ByVal e As EventArgs) _ Crea il proxy. Ottiene l’argomento. Invoca il metodo remoto. Visualizza il risultato.

Figura 26-8. L’applicazione client d’esempio.

Nota

Durante la fase di collaudo, capiterà spesso di modificare la struttura dell’XML Web service – ad esempio, aggiungendo nuovi metodi o modificando la firma di quelli esistenti. Dopo ogni cambiamento all’interfaccia della classe, bisognerebbe ricreare la classe proxy nel progetto client. A questo proposito, il modo più semplice consiste nell’utilizzare il comando Update Web Reference del menu di contesto di ciascun riferimento (localhost, se non è stato rinominato) contenuto nella cartella Web References della finestra Solution Explorer.

I protocolli degli XML Web service Come ho già detto, gli XML Web service realizzati con ASP.NET gestiscono automaticamente tre protocolli: HTTP GET, HTTP POST e SOAP. In pratica, tuttavia, sarà quest’ultimo a essere utilizzato nella maggior parte delle applicazioni e gli altri due andrebbero considerati come protocolli ausiliari che consentono di collaudare gli XML Web service dal browser o invocarne i metodi da applicazioni ASP legacy. La pagina di help creata da ASP.NET per gli XML Web service contiene indicazioni sulla mole di informazioni inviata e restituita da un XML Web service utilizzando i tre protocolli in questione (figura 26-3).


Capitolo 26 Gli XML Web service

1135

Il protocollo HTTP GET Per invocare un XML Web service si utilizza il protocollo HTTP GET passando tutti gli argomenti nella query string, come mostra la figura 26-4. Quando si invoca il metodo DollarToEuro tramite il metodo HTTP GET, questo è il testo che viene inviato alla pagina .asmx: GET /MoneyConverter/Converter.asmx/DollarToEuro?amount=180 HTTP/1.1 Host: localhost

e questo è ciò che viene restituito al chiamante: HTTP/1.1 200 OK Content-Type: text/xml; charset=utf-8 Content-Length: 94 <?xml version=”1.0" encoding=”utf-8"?> <decimal xmlns=”http://www.vb2themax.com/”>200</decimal>

Il protocollo HTTP GET impedisce di passare strutture e oggetti come argomenti; non consente nemmeno di passare argomenti ByRef, anche se è lecito restituire un oggetto (a patto che possa essere serializzato in XML). Oltre a ciò, è necessario codificare gli argomenti che contengono caratteri particolari, come ad esempio il simbolo =. Come se non bastasse, è possibile accedere facilmente a un XML Web service tramite HTTP GET anche da applicazioni legacy, come quelle scritte in Visual Basic 6 o con ASP. La routine Visual Basic 6 che segue utilizza, ad esempio, la componente MSXML per fornire un wrapper attorno al metodo DollarToEuro: ‘ Una routine Visual Basic 6 ‘ NOTA: richiede un riferimento alla type library Microsoft XML, v3.0. Function ConvertDollarToEuro(ByVal amount As Currency) As Currency ‘ Questo è l’URL dell’XML Web service. Const url As String = “http://localhost/MoneyConverter/Converter.asmx/” _ & “DollarToEuro?amount=” Dim httpReq As New MSXML2.XMLHTTP30 ‘ Aggiunge l’argomento alla query string ed effettua una richiesta ‘ sincrona. httpReq.open “GET”, url & CStr(amount), False ‘ Invia la richiesta HTTP-GET httpReq.send ‘ Legge il testo XML restituito. Dim xmlResp As MSXML2.DOMDocument30 Set xmlResp = httpReq.responseXML ‘ Il risultato è il testo dell’elemento XML del documento principale. ConvertDollarToEuro = CCur(xmlResp.documentElement.Text) End Function

Questo codice può essere convertito anche in VBScript e invocato da ASP. Questa operazione è talmente semplice che la lascerò come esercizio.

Il protocollo HTTP POST Il protocollo HTTP POST è simile ad HTTP GET. La differenza fondamentale tra i due risiede nel fatto che gli argomenti del primo vengono passati nel body dell’HTTP invece che nella query string. Come accade per il protocollo HPPT GET, anche nel caso di HTTP POST non è consentito passare argomenti ByRef, strutture od oggetti, e in pratica tale protocollo andrebbe preso in considerazione solo nel caso in cui l’XML Web service venga invocato da un’applicazione legacy. Quello che segue è un esempio di una chiamata al metodo DollarToEuro attraverso il protocollo HTTP POST: POST /MoneyConverter/Converter.asmx/DollarToEuro HTTP/1.1 Host: localhost


1136

Parte VI Applicazioni Internet

Content-Type: application/x-www-form-urlencoded Content-Length: 13 amount=string

In questo caso, non vale la pena mostrare il testo restituito dal client in quanto è identico a quello visto nel caso del protocollo HTTP GET. Come quest’ultimo, anche HTTP POST è in grado di invocare un metodo che restituisce un oggetto serializzabile.

Il protocollo SOAP La maggior parte degli XML Web service reali dovrebbe utilizzare il protocollo SOAP, il quale utilizza messaggi SOAP sia per gli argomenti di input sia per il valore di ritorno. Il protocollo SOAP non presenta nessuna delle limitazioni viste per HTTP GET e POST. Il nodo radice di ciascun messaggio SOAP rappresenta l’involucro (envelope) del messaggio, al cui interno è contenuto il corpo (body) del messaggio; a sua volta, quest’ultimo contiene un tag XML avente lo stesso nome del metodo di destinazione, e tutti gli argomenti vengono inviati all’interno di tale blocco. Ad esempio, quello che segue è il testo inviato all’XML Web service quando il metodo DollarToEuro viene invocato attraverso il protocollo SOAP: POST /MoneyConverter/Converter.asmx HTTP/1.1 Host: localhost Content-Type: text/xml; charset=utf-8 Content-Length: 341 SOAPAction: “http://tempuri.org/DollarToEuro” <?xml version=”1.0" encoding=”utf-8"?> <soap:Envelope xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” xmlns:xsd=”http://www.w3.org/2001/XMLSchema” xmlns:soap=”http://schemas.xmlsoap.org/soap/envelope/”> <soap:Body> <DollarToEuro xmlns=”http://tempuri.org/”> <amount>180</amount> </DollarToEuro> </soap:Body> </soap:Envelope>

Il testo restituito dal metodo è un altro messaggio SOAP dotato di propri blocchi Envelope e Body. In questo caso, tuttavia, il corpo contiene un tag denominato NomeMetodoResponse, che a sua volta contiene un tag annidato denominato NomeMetodoResult, il cui testo è il valore di ritorno: HTTP/1.1 200 OK Content-Type: text/xml; charset=utf-8 Content-Length: 381 <?xml version=”1.0" encoding=”utf-8"?> <soap:Envelope xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” xmlns:xsd=”http://www.w3.org/2001/XMLSchema” xmlns:soap=”http://schemas.xmlsoap.org/soap/envelope/”> <soap:Body> <DollarToEuroResponse xmlns=”http://tempuri.org/”> <DollarToEuroResult>200</DollarToEuroResult> </DollarToEuroResponse> </soap:Body> </soap:Envelope>

Ciascun messaggio SOAP può contenere un’intestazione opzionale, nella quale potrebbero esserci informazioni aggiuntive non necessariamente correlate al metodo che viene invocato. Ad esempio, un client potrebbe utilizzare l’intestazione per inviare le proprie credenziali in modo che l’XML Web service possa registrare chi accede ai propri metodi e quando. Il corpo di un messaggio SOAP può essere sostituito da una sezione <soap:Fault> se il messaggio contiene informazioni d’errore, come


Capitolo 26 Gli XML Web service

1137

nel caso venga invocato un metodo passando un numero errato di argomenti o quando il codice del metodo ha sollevato un’eccezione. SOAP è l’unico protocollo in grado di passare oggetti e strutture come argomenti. L’unico requisito perché un oggetto possa essere passato a un XML Web service o restituito da questo è che sia serializzabile. Oltre a ciò, solo il protocollo SOAP gestisce gli argomenti di output. In altre parole, se il metodo accetta uno o più argomenti ByRef, è possibile invocarlo solo tramite il protocollo SOAP. Per provare facilmente questo punto, basta modificare la firma del metodo DollarToEuro e sostituire la parola chiave ByVal con ByRef. Se si consulta la pagina di help che mostra la sintassi gestita dal metodo in questione, si potrà vedere che l’unico protocollo nominato è proprio SOAP.

Nota

Il resto del capitolo è dedicato interamente agli XML Web service basati su SOAP.

Abilitare i protocolli nei file di configurazione Di norma, gli XML Web service di .NET sono in grado di gestire tutti e tre i protocolli. Tuttavia, è possibile disabilitare uno o più di tali protocolli modificando il file machine.config oppure il file web.config locale del file .asmx che si desidera controllare. Il file machine.config predefinito contiene la sezione che segue, la quale abilita tutti e tre i protocolli e consente a un XML Web service di autodocumentarsi automaticamente nel browser: <configuration> <system.web> <webServices> <protocols> <add name=”HttpSoap” /> <add name=”HttpPost” /> <add name=”HttpGet” /> <add name=”Documentation” /> </protocols> </webServices> </system.web> </configuration>

In un XML Web service di produzione, si potrebbe decidere di eliminare tutti i tag <add> a eccezione di quello che definisce il supporto per il protocollo SOAP e possibilmente quello che abilita la creazione automatica della documentazione.

Creare XML Web service senza Visual Studio Come tutti sanno, ciò che occorre per sfruttare la potenza del .NET Framework viene fornito con il .NET Framework SDK, compreso tutto il necessario per realizzare XML Web service. Anche se si ha intenzione di utilizzare Visual Studio per la maggior parte dell’attività di sviluppo, è bene sapere anche come creare XML Web service con Notepad e con le utility a riga di comando installate con .NET. Se non altro, ciò servirà ad apprezzare ancor di più ciò che Visual Studio realizza dietro le quinte. In questa sezione, si vedrà anche come creare un file .disco, il quale consente ai client remoti di individuare tutti gli XML Web service ospitati sul computer locale. Questa è un’operazione che va eseguita indipendentemente dal fatto che si decida di creare gli XML Web service con Visual Studio.

Creare il file .asmx Il primo passo per creare da zero un XML Web service consiste nello scrivere il codice sorgente per il servizio utilizzando un qualsiasi editor, incluso Notepad. Come accade per le pagine .aspx, anche i file .asmx vengono compilati quando il client li richiede per la prima volta. ASP.NET gestisce anche le classi di code-behind per i file .asmx. Quando si scrive un file .asmx con un editor di testo, bisognerebbe aggiunge-


1138

Parte VI Applicazioni Internet

re una direttiva @WebService i cui attributi indicano il nome della classe e il linguaggio utilizzato. Quello che segue è una semplice classe MathService che contiene due metodi per aggiungere e sottrarre interi: <%@ WebService Language=”VB” Class=”MathService” %> Imports System Imports System.Web.Services < WebService (Description:=”Yet another math web service”, _ Namespace:=”www.vb2themax.com”)> _ Public Class MathService Inherits WebService <WebMethod (Description:=”Add two Integers”)> _ Public Function Add(ByVal n1 As Integer, ByVal n2 As Integer) _ As Integer Return n1 + n2 End Function <WebMethod (Description:=”Subtract two Integers”)> _ Public Function Subtract(ByVal n1 As Integer, ByVal n2 As Integer) _ As Integer Return n1 - n2 End Function End Class

È possibile salvare il codice precedente nel file MathService.asmx all’interno della directory radice di IIS e navigare fino a quel punto utilizzando il browser. La figura 26-9 mostra la pagina di help usuale, dalla quale è possibile collaudare entrambi i metodi. Confrontando questa figura con la 26-2, si potrà vedere che il warning relativo al namespace predefinito (tempuri.org) è stato eliminato in quanto la pagina .asmx contiene un namespace in grado di garantire che le relative classi possiedano un nome univoco su Internet. Ad esempio, nell’attributo Namespace si potrebbe utilizzare l’indirizzo del sito Web della propria società.

Figura 26-9. La pagina di help omette il warning se l’XML Web service utilizza un namespace specifico.

È possibile navigare nel contratto WSDL aggiungendo ?wsdl alla query string, come in: http://localhost/MathService.asmx?wsdl

Abilitare il processo di discovery degli XML Web service Nel caso sia stato realizzato un servizio utile, probabilmente si vuole consentire ad altri di accedervi. Un programmatore può scaricare facilmente il file .wsdl contenente il contratto aggiungendo ?wsdl all’URL dell’XML Web service, oppure può seguire la procedura descritta nel paragrafo “Creare un client per


Capitolo 26 Gli XML Web service

1139

un XML Web service”. Tuttavia, cosa accade se il programmatore non conosce l’esatto URL dell’XML Web service? È proprio qui che il processo di discovery dell’XML Web service entra in gioco. Il meccanismo di discovery consente di determinare l’URL tramite il quale è possibile trovare uno o più XML Web service localizzati su un server specifico o distribuiti in Internet. Esistono diversi tipi di discovery, tra cui: ■ Discovery statica Viene creato un file XML .disco che elenca tutti gli XML Web service disponibili sulla macchina locale, e salvato nella directory radice del server Web. ■ Discovery dinamica Viene creato un file XML denominato default.vsdisco e salvato nella directory radice del server Web. Questo file consente ai client remoti di individuare tutti gli XML Web service installati sul computer, ma è possibile elencare una serie di sottodirectory che questo tipo di processo deve ignorare. Per questioni di sicurezza, non bisognerebbe mai abilitare il processo di discovery dinamica sui server di produzione, e utilizzarlo esclusivamente su server di sviluppo e test. ■ Universal Description, Discovery, and Integration (UDDI) Come ho spiegato nel paragrafo “Creare un client per un XML Web Service”, UDDI offre un modo standard per esporre informazioni sugli XML Web service distribuiti in Internet. In questo paragrafo verrà trattata esclusivamente la discovery statica. Per ulteriori informazioni sulla discovery dinamica si può consultare la documentazione di Visual Studio .NET, mentre per una descrizione dettagliata di UDDI si può consultare il sito www.uddi.org. Per ogni progetto XML Web service dovrebbe esserci un file .disco che descrive la locazione della pagina di help e del file del contratto .wsdl corrispondenti. Come tutti sanno, ASP.NET crea una pagina di help standard al volo quando si naviga al file .asmx, ed è possibile creare un contratto WSDL aggiungendo semplicemente ?wsdl all’URL del file .asmx. Tuttavia, in alcuni casi si può voler fornire un file .htm personalizzato che spieghi come utilizzare l’XML Web service, e un file .wsdl statico che contenga il relativo contratto WSDL; da qui la necessità di creare un file .disco. Per vedere di cosa si tratta, si può chiedere ad ASP.NET di creare un file .disco al volo aggiungendo semplicemente ?disco all’URL di un file .asmx: http://localhost/MathService.asmx?disco

Questo è ciò che appare nel browser: <?xml version=”1.0" encoding=”utf-8" ?> <discovery xmlns:xsd=”http://www.w3.org/2001/XMLSchema” xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance” xmlns=”http://schemas.xmlsoap.org/disco/”> <contractRef ref=”http://localhost/mathservice.asmx?wsdl” docRef=”http://localhost/mathservice.asmx” xmlns=”http://schemas.xmlsoap.org/disco/scl/” /> <soap address=”http://localhost/mathservice.asmx” xmlns:q1=”www.vb2themax.com” binding=”q1:MathServiceSoap” xmlns=”http://schemas.xmlsoap.org/disco/soap/” /> </discovery>

L’attributo ref nel tag <contractRef> specifica la locazione del file .wsdl, mentre l’attributo docRef indica dove l’utente può leggere una descrizione dell’XML Web service; pertanto è possibile aumentare le prestazioni del sito facendo in modo che tali attributi puntino ai file fisici creati appositamente, invece di obbligare ASP.NET a crearli dinamicamente a ogni richiesta. A questo punto si è pronti a creare un file .disco principale che descriva tutti gli XML Web service disponibili sul server Web (uno solo, fino a ora). Per convenzione questo file viene denominato default.disco. Esso contiene un elemento radice <disco:discovery>, che a sua volta contiene una o più coppie di tag <discoveryRef> e <contractRef> che, presi assieme, puntano a tutti i file .disco, .wsdl e alle pagine di help di tutti gli XML Web service disponibili sulla macchina locale. Ad esempio, quello che segue potrebbe essere il file default.disco che descrive i due XML Web service creati in precedenza:


1140

Parte VI Applicazioni Internet

<?xml version=”1.0" ?> <disco:discovery xmlns:disco=”http://schemas.xmlsoap.org/disco/”> <discoveryRef ref=”/MathService.asmx?disco” /> <contractRef ref=”http:/MathService.asmx?WSDL” docRef=”/MathService.asmx” xmlns=”http://schemas.xmlsoap.org/disco/scl/” /> <discoveryRef ref=”/MoneyConverter/Converter.asmx?disco” /> <contractRef ref=”http:/MoneyConverter/converter.asmx?WSDL” docRef=”ConverterService.htm” xmlns=”http://schemas.xmlsoap.org/disco/scl/” /> </disco:discovery>

Il file default.disco dovrebbe essere salvato nella directory radice del server Web, in modo che chiunque possa scaricarlo. Ad esempio, se ora si inserisce l’URL http://localhost/default.disco nella finestra di dialogo Add Web Reference di Visual Studio, è possibile visualizzare entrambi gli XML Web service MathService e MoneyConverter (figura 26-10).

Figura 26-10. Il file default.disco consente di visualizzare il contratto e la documentazione di tutti gli XML Web service che si vogliono esporre al mondo esterno.

Rimane un’ultima cosa da fare per aiutare gli altri sviluppatori a individuare e a utilizzare il file default.disco: renderlo il file predefinito per il server Web. A questo proposito si seleziona la pagina Documents della finestra di dialogo Default Web Site Properties di IIS e si preme il pulsante Add per aggiungere un nuovo documento di default (figura 26-11). Se default.disco rappresenta il documento predefinito del server Web, è possibile elencare tutti gli XML Web service all’interno della finestra di dialogo Add Web Reference digitando semplicemente http://localhost. In molti casi, tuttavia, dal momento che un server Web è in grado di ospitare sia un sito Web standard (che genera testo HTML) sia uno o più XML Web service, non sarà sufficiente rendere il file default.disco il documento predefinito per l’intero sito, in quanto questo dovrebbe essere il documento che i visitatori ricevono quando digitano l’URL del sito nei rispettivi browser. La soluzione a questo problema è semplice: occorre inserire nel file .htm o .aspx un tag <link> particolare, come segue:


Capitolo 26 Gli XML Web service

1141

Figura 26-11. È possibile rendere il file default.disco il documento predefinito del server Web. <HTML> <HEAD> <LINK TYPE=’text/xml’ REL=’alternate’ HREF=’default.disco’/> </HEAD> <BODY> Welcome to my default page. </BODY> </HTML>

Utilizzare la utility Disco Nel paragrafo precedente si è visto come creare un file .disco e utilizzarlo per individuare da Visual Studio tutti gli XML Web service disponibili sulla macchina locale. Tuttavia, come si sa, tutto ciò che viene eseguito all’interno di Visual Studio può essere realizzato anche con le utility fornite dal .NET Framework. Il passaggio successivo consiste nell’utilizzare la utility disco.exe per individuare ogni XML Web service registrato a un determinato URL. Il comando che segue individua gli XML Web service sulla macchina locale, ma può essere utilizzato anche nel caso di server remoti: DISCO http://localhost/default.disco

(Se default.disco rappresenta il documento predefinito per il sito Web, è possibile ometterlo e digitare semplicemente l’URL del sito). Se il processo di discovery ha successo, la utility Disco scarica il file .disco e i file .wsdl a cui questo punta, e crea un file resutls.discomap che contiene i collegamenti a ognuno dei file in questione (figura 26-12). Altre opzioni a riga di comando consentono di salvare i file scaricati in directory differenti oppure di accedere a server Web che richiedono l’autenticazione. (Per un elenco completo delle opzioni disponibili, basta digitare Disco/?). È anche possibile creare il file .wsdl di un XML Web service passando l’URL relativo completo come argomento alla utility disco: DISCO http://localhost/MathService.asmx

Come sarà possibile vedere a breve, il file .wsdl è indispensabile per creare la classe proxy per il client.


1142

Parte VI Applicazioni Internet

Figura 26-12. Utilizzare la utility Disco.

Utilizzare la utility Wsdl Una volta ottenuto un file .wsdl che contiene la descrizione della classe XML Web service e dei relativi metodi, è possibile utilizzare la utility Wsdl.exe per generare il codice sorgente della classe proxy utilizzabile dai client per accedere a XML Web service remoti. La sintassi è semplice: WSDL MathService.wsdl /l:vb

dove l’opzione /l specifica il linguaggio da utilizzare per la classe proxy. (Se omesso, il default è C#). A meno di non utilizzare l’opzione /out per modificare il file di destinazione, la utility Wsdl crea un file .vb o .cs avente lo stesso nome del file .wsdl (MathService.vb, in questo esempio). La classe .vb creata dal precedente comando è esattamente identica a quella realizzata utilizzando il comando Add Web Reference all’interno di Visual Studio. La utility Wsdl presenta un’ulteriore caratteristica che non trova corrispondenti in Visual Studio. Se si passa l’opzione /server, Wsdl crea una classe MustInherit che espone gli stessi metodi della classe XML Web service originale invece di creare la classe proxy per il client: WSDL MathService.wsdl /l:vb /server /out:BaseClass.vb

Quello che segue è il contenuto del file BaseClass.vb, dopo aver eliminato tutti i commenti e gli attributi superflui: Public MustInherit Class MathService Inherits System.Web.Services.WebService <System.Web.Services.WebMethodAttribute()> _ Public MustOverride Function Add(ByVal n1 As Integer, _ ByVal n2 As Integer) As Integer <System.Web.Services.WebMethodAttribute() > _ Public MustOverride Function Subtract(ByVal n1 As Integer, _ ByVal n2 As Integer) As Integer End Class

Tanto la utility disco quanto la Wsdl mettono a disposizione numerose opzioni che non è stato possibile trattare in questo contesto: ad esempio, le opzioni /domain, /username e /password per accedere a server protetti. Per ottenere ulteriori informazioni a riguardo, è possibile eseguire la utility con l’opzione ?, oppure consultare la documentazione del .NET Framework SDK.

Gli XML Web service visti da vicino Ciò che è stato descritto fino a questo punto consente di creare XML Web service utili e funzionali, ma, naturalmente, c’è ancora molto altro da dire. In questa sezione, sarà possibile vedere come migliorare le prestazioni degli XML Web service tramite il caching, come gestire le eccezioni e come sfruttare i vantaggi dell’infrastruttura di ASP.NET.


Capitolo 26 Gli XML Web service

1143

La classe XML Web service La classe che espone i metodi degli XML Web service generalmente deriva da System.Web.Services. WebService, anche se ciò, in realtà, non è obbligatorio in quanto la presenza dell’attributo WebMethod è già sufficiente a far sì che uno o più metodi della classe possano essere invocati da client remoti. In pratica, tuttavia, si preferisce creare sempre una classe XML Web service che derivi dalla classe di base WebService perché tale soluzione consente di accedere al contesto di ASP.NET e ad altri oggetti utili, come le variabili di tipo Application e Session.

L’attributo WebService Come ho appena spiegato, l’attributo WebService è opzionale nel senso che ASP.NET è sempre in grado di determinare se una classe può fungere da XML Web service o meno. Tuttavia, bisognerebbe utilizzare questo attributo per specificare un namespace diverso da http://tempuri.org/ prima che l’XML Web service diventi pubblico. Il costruttore per l’attributo WebService può accettare tre argomenti: Description, Namespace e Name. Si è già visto come utilizzare i primi due: <WebService(Description:=”A web service for converting currencies”, Namespace:=”http://www.vb2themax.com/”) > _ Public Class Converter Inherits System.Web.Services.WebService § End Class

Il valore namespace rappresenta solo un mezzo per rendere univoco il nome dell’XML Web service e non deve corrispondere a un URL fisico. La convenzione, utilizzata praticamente da tutti, di utilizzare l’URL del sito Web della propria società garantisce che la combinazione namespace+nome sia univoca. L’argomento Name influisce sul nome della classe che appare nel contratto WSDL e in ultimo diventa il nome della classe proxy che viene creata con il comando Add Web Reference di Visual Studio o con la utility Wsdl. Nel caso tale argomento venga omesso, la classe proxy assumerà lo stesso nome della classe XML Web service originale: <WebService(Description:=”A web service for converting currencies”, Namespace:=”http://www.vb2themax.com/”, Name:=”MoneyConv”) > _ Public Class Converter Inherits System.Web.Services.WebService § End Class

Accedere agli oggetti di ASP.NET Uno dei vantaggi di creare un XML Web service derivando una classe da System.Web.Services.WebService è la possibilità di accedere a ogni oggetto intrinseco di ASP.NET dall’interno della classe. Più precisamente, la classe di base WebService espone le seguenti proprietà: ■ Le proprietà Application, Session e Server restituiscono gli oggetti di ASP.NET omonimi. L’oggetto Application può essere utilizzato per memorizzare nella cache i valori comuni a tutti i client, proprio come accade nel caso delle applicazioni Web Form. Bisogna tener presente che l’oggetto Session è disponibile solo se è stato specificato l’argomento EnableState nell’attributo WebMethod, come verrà spiegato nel paragrafo “Abilitare lo stato della sessione” più avanti in questo capitolo. ■ La proprietà User restituisce l’oggetto IPrincipal che rappresenta un utente autenticato. Ad esempio, questa proprietà restituisce un oggetto WindowsPrincipal nel caso in cui l’utente venga autenticato attraverso l’autenticazione di Windows. Per decidere quale meccanismo di autenticazione debba gestire l’XML Web service, occorre modificare il file web.config per l’applicazione XML Web


1144

Parte VI Applicazioni Internet

service, proprio come nel caso di applicazioni Web Form. (Per maggiori dettagli sull’autenticazione in ASP.NET, si rimanda al paragrafo “La sicurezza di ASP.NET” del capitolo 24). ■ La proprietà Context restituisce l’oggetto HttpContext per la richiesta corrente. Tale oggetto espone i cinque oggetti intrinseci di ASP.NET, l’oggetto Trace, l’oggetto Error e qualche altra proprietà. Tra gli oggetti più utili ai quali è possibile accedere da un XML Web service vi sono Application e Cache, con i quali è possibile memorizzare i valori nella cache e condividerli tra i client. Quello che segue è un esempio molto semplice che utilizza una variabile Application per tener traccia del numero di volte che un determinato metodo è stato invocato: <WebMethod(Description:=”Return the server time”)> _ Function GetTime(ByVal arg As Integer) As Date Me.Application.Lock() Me.Application(“GetTimeCounter”) = GetTimeCounter() + 1 Me.Application.Unlock() Return Date.Now End Function <WebMethod(Description:=”The number of times GetTime has been called”)> _ Function GetTimeCounter() As Integer Dim o As Object = Me.Application(“GetTimeCounter”) If o Is Nothing Then Return 0 Else Return CInt(o) End If End Function

Per rivedere le modalità di utilizzo degli oggetti Application e Cache, si può tornare ai paragrafi “La classe HttpApplicationState” e “La classe Cache” del capitolo 24.

Tipi di dato semplici e complessi Il grande vantaggio degli XML Web service è la capacità di ricevere e restituire qualsiasi tipo di dato di .NET, inclusi i tipi personalizzati, a patto che siano serializzabili. (La serializzazione degli oggetti è stata descritta nel capitolo 11). Ad esempio, è possibile passare e restituire oggetti DataSet di ADO.NET – sia generici sia fortemente tipizzati – in quanto possono, appunto, essere serializzati. Nella maggior parte dei casi, non occorre nulla di speciale per eseguire il marshalling di un tipo di dato in quanto la definizione di tutti i tipi ricevuti o restituiti da un progetto XML Web service, ed esposti come argomenti o valori di ritorno, è inclusa nel contratto WSDL. Ogni volta che si aggiunge un riferimento Web in Visual Studio o si esegue la routine Wsdl, viene creata una classe proxy per ognuno di questi tipi in modo che il client possa utilizzarli. In qualche caso, tuttavia, è necessario obbligare Visual Studio o la utility Wsdl a includere i tipi corretti nel contratto WSDL. Si consideri, ad esempio, la seguente classe XML Web service: Imports System.Xml.Serialization <WebService(Description:=”A web service with methods for doing tests”, _ Namespace:=”http://www.vb2themax.com/”)> _ Public Class SampleService Inherits System.Web.Services.WebService <WebMethod())> _ Function GetDocument(ByVal docname As String) As Document Select Case docname.ToLower Case “invoice” Return New Invoice() Case “purchaseorder” Return New PurchaseOrder()


Capitolo 26 Gli XML Web service

1145

End Select End Function End Class Public MustInherit Class Document Public [Date] As Date Public Number As Integer End Class Public Class Invoice Inherits Document Public Total As Decimal End Class Public Class PurchaseOrder Inherits Document Public AuthorizedBy As String End Class

Il problema in questo listato è che il metodo GetDocument può restituire un oggetto Invoice o un PurchaseOrder, sebbene il contratto WSDL conterrà esclusivamente la definizione della classe astratta Document. Per obbligare il contratto WSDL a includere la definizione delle due classi concrete, è necessario contrassegnare il metodo con l’attributo SoapRpcMethod e aggiungere due istanze dell’attributo SoapInclude, come segue: <WebMethod(),SoapRpcMethod(),SoapInclude(GetType(Invoice)), _ SoapInclude(GetType(PurchaseOrder))> _ Function GetDocument(ByVal docname As String) As Document § End Function

L’attributo SoapInclude potrebbe essere impiegato anche in altri casi simili: ad esempio, quando un metodo accetta o restituisce un ArrayList o una collezione di oggetti, e il nome effettivo della classe dell’oggetto restituito non appare esplicitamente all’interno della firma del metodo pubblico: ‘ Il metodo che segue accetta un ArrayList di oggetti Book ma nessun altro ‘ metodo accetta o restituisce esplicitamente un oggetto Book. <WebMethod(),SoapRpcMethod(),SoapInclude(GetType(Book)) > _ Sub ProcessProducts(ByVal books As ArrayList) § End Function

Il namespace System.Xml.Serialization include altri attributi Soapxxxx, come SoapType, SoapElement, SoapAttributeAttribute, SoapIgnore e SoapEnum. Questi attributi possono essere utilizzati per definire la serializzazione di classi e strutture in XML e si comportano praticamente come gli attributi Xmlxxxx descritti nel paragrafo “Serializzazione XML” del capitolo 11. A meno di non dover controllare la forma dell’XML inviato o ricevuto dall’XML Web service, non sarà necessario applicare questi attributi per poter utilizzare gli XML Web service.

L’attributo WebMethod A differenza dell’attributo WebService, l’attributo WebMethod è invece obbligatorio e deve essere utilizzato per contrassegnare tutti i metodi da rendere accessibili ai client. Questa soluzione consente di realizzare una componente che espone un insieme di metodi ai relativi client locali, ma fa sì che solo un sottoinsieme di tali metodi risulti disponibile ai client remoti che si connettono tramite un XML Web service. Tutti gli argomenti di questo attributo sono facoltativi, ma in pratica bisognerebbe almeno specificare la descrizione per il metodo, la quale apparirà nella pagina di help dell’XML Web service: <WebMethod(Description:=”Convert from Euro to Dollar currency”)> _ Function EuroToDollar(ByVal amount As Decimal) As Decimal


1146

Parte VI Applicazioni Internet

Return amount * GetEuroToDollarConversionRate() End Function

Utilizzare i metodi di overload L’argomento MessageName specifica il nome che i client possono utilizzare per invocare il metodo. Tale argomento è necessario solo quando la classe XML Web service espone metodi di overload con lo stesso nome. Dal momento che l’infrastruttura dell’XML Web service non è in grado di risolvere le chiamate ai metodi di overload, sarà necessario specificare, per questi ultimi, nomi esterni differenti: <WebMethod (Description:=”Add two Integers”)> _ Public Function Add(ByVal n1 As Integer, ByVal n2 As Integer) _ As Integer Return n1 + n2 End Function <WebMethod (Description:=”Add two floating point numbers”, _ MessageName:=”AddDouble”)> _ Public Function Add(ByVal n1 As Double, ByVal n2 As Double) _ As Double Return n1 + n2 End Function

Bufferizzare le risposte L’argomento BufferResponse assomiglia alla proprietà BufferOutput dell’oggetto HttpResponse, nel senso che influisce sul fatto che l’output dell’XML Web service venga inviato al client come un unico blocco XML oppure suddiviso in blocchi più piccoli che vengono spediti durante la serializzazione in XML dell’oggetto restituito. In pratica, questo argomento sarà generalmente impostato a True (il valore predefinito) e verrà modificato a False solo nel caso vengano restituiti blocchi XML di grandi dimensioni, come quando si restituisce un DataSet di grandi dimensioni al client: <WebMethod(BufferResponse:=”False”)> _ Public Function GetPublishers() As DataSet § End Function

Quando BufferResponse è False, le estensioni SOAP per il metodo risultano disabilitate. (Per una descrizione sulle estensioni SOAP si rimanda al paragrafo “Le estensioni SOAP”).

Memorizzare i risultati nella cache Dal momento che si tratta di applicazioni ASP.NET, gli XML Web service gestiscono il caching dell’output proprio come i controlli Web Form e Web User. Gli XML Web service non gestiscono, però, la direttiva @OutputCache, per cui l’output di ogni singolo metodo può essere memorizzato nella cache per un certo intervallo di tempo tramite l’argomento CacheDuration dell’attributo WebMethod. Ad esempio, il codice che segue memorizza l’output del metodo GetPublishers per un minuto: <WebMethod(CacheDuration:=60)> _ Public Function GetPublishers() As DataSet § End Function

Se il metodo accetta degli argomenti, ASP.NET mantiene nella cache una versione diversa per ogni combinazione dei valori degli argomenti. Ad esempio, il metodo seguente memorizza un’immagine per ogni valore distinto dell’argomento State che viene passato al metodo: <WebMethod(CacheDuration:=60, MesssageName:=”GetPublishersByState”)> _ Public Function GetPublishers(ByVal State As String) As DataSet § End Function


Capitolo 26 Gli XML Web service

1147

Se utilizzato in modo appropriato, l’attributo CacheDuration è in grado di migliorare le prestazioni dell’XML Web service più di qualsiasi altra tecnica disponibile. Tuttavia, sarebbe bene valutare sempre con attenzione l’impatto sul consumo delle risorse sul server. Questa tecnica risulta particolarmente efficace per risultati di piccole dimensioni che richiedono una notevole elaborazione sul server – ad esempio, il calcolo di statistiche sui database – ma possono diventare un collo di bottiglia nel caso in cui il metodo accetti argomenti i cui valori possono variare considerevolmente o restituisca una grande quantità di dati.

Abilitare lo stato della sessione Gli XML Web service, come tutte le applicazioni .NET, possono sfruttare i vantaggi offerti dalla gestione dello stato della sessione, anche se questa, di norma, risulta disabilitata in quanto la maggior parte delle chiamate agli XML Web service sono prive di stato e non hanno bisogno di memorizzare i dati all’interno di una variabile Session. Questa soluzione evita la necessità di popolare la collezione Session a ogni chiamata, riduce il consumo di memoria sul server e impedisce l’affinità di server. Qualche volta, tuttavia, può risultare utile o interessante abilitare le variabili Session: ad esempio, in un XML Web service che espone un metodo per ordinare i singoli prodotti e un altro metodo per confermare l’ordine, calcolare il costo della spedizione e restituire il totale dell’acquisto. Un XML Web service di questo tipo non è privo di stato e deve memorizzare i dettagli dell’ordine in corso in una o più variabili Session. Per abilitare la gestione della sessione occorre impostare l’argomento EnableSessione dell’attributo WebMethod a True; se questo argomento viene omesso, oppure è impostato a False, ogni riferimento all’oggetto Session solleverà un’eccezione NullReference. Quello che segue è un semplice esempio di un metodo di un XML Web service basato sulle variabili Session: <WebMethod(EnableSession:=True)> _ Function IncrementCounter() As Integer If Session(“counter”) Is Nothing Then Session.Add(“counter”, 1) Else Session(“counter”) = CInt(Session(“counter”)) + 1 End If Return CInt(session(“counter”)) End Function <WebMethod(EnableSession:=True)> _ Function GetSessionID() As String Return Session.SessionId End Function

Gli XML Web service che si basano sullo stato della sessione presentano un grave difetto. Se si collauda il metodo IncrementCounter dal browser utilizzando la pagina di help standard, si vedrà che ogni chiamata al metodo incrementa il risultato, provando così che lo stato della sessione viene correttamente mantenuto tra le diverse chiamate. A ulteriore riprova, si può verificare che il valore di ritorno del metodo GetSessionID non cambia, almeno fino a che non si chiude il browser o la sessione termina dopo il suo naturale timeout (di default, 20 minuti di inattività). Tuttavia, nel caso in cui il metodo venga invocato da un’applicazione Windows Form che utilizza una classe proxy, occorre essere preparati a una sorpresa piuttosto spiacevole. In questo caso, infatti, il metodo IncrementCounter restituisce sempre 1 e il metodo GetSessionID restituisce, invece, una stringa diversa a ogni invocazione. Per spiegare questo insolito comportamento è necessario ricordare che la gestione della sessione dipende da un cookie non persistente che viene spedito al client quando questo invia per la prima volta una richiesta all’applicazione ASP.NET. Se il client è un browser, il cookie della sessione viene mantenuto in memoria e rinviato a ogni richiesta successiva in modo che l’applicazione sia in grado di riconoscere il client e associarlo all’opportuno insieme di variabili Session. Se, tuttavia, il client è un’applicazione Windows Form, il cookie della sessione non potrà essere memorizzato e ogni


1148

Parte VI Applicazioni Internet

nuova richiesta del client verrà trattata come se fosse la prima. Nel paragrafo “La proprietà CookieContainer” più avanti in questo capitolo, sarà possibile vedere come aggirare questo problema.

Creare XML Web service transazionali L’argomento TransactionOption dell’attributo WebMethod consente di creare XML Web service transazionali, i quali possono invocare componenti COM+ transazionali e sfruttare i vantaggi del supporto offerto al Microsoft Distribuited Transaction Coordinator (MS DTC). Le componenti transazionali non fanno parte degli argomenti trattati in questo libro, pertanto descriverò questo attributo solo per questioni di completezza. A causa della natura priva di stato degli XML Web service, un metodo di un XML Web service può rappresentare soltanto la radice di una transazione. In altre parole, non è possibile avviare una transazione in un client – che potrebbe essere un’applicazione Windows Form, una pagina Web Form o un altro metodo di un XML Web service – e propagarla attraverso una chiamata all’XML Web service. A questo riguardo, gli XML Web service transazionali si comportano come le pagine ASP e ASP.NET transazionali. L’argomento TransactionOption può accettare cinque valori differenti – Disabled, NotSupported, Supported, Required e RequiresNew – ma a causa della limitazione appena menzionata, sono ammessi solo due comportamenti diversi. Per ottenere un XML Web service transazionale occorre specificare Required o RequiresNew, mentre gli altri valori valgono per gli XML Web service standard, ossia non transazionali. Quella che segue è la tipica struttura di un metodo di un XML Web service transazionale: ‘ Questo codice assume di aver aggiunto la seguente istruzione Imports: ‘ Imports System.Data.OleDb <WebMethod(TransactionOption:=TransactionOption.RequiresNew)> _ Function UpdateDatabase() As Boolean Dim cn As New OleDbConnection(myConnString) Try cn.Open() ‘ Esegue tutte le necessarie operazioni di aggiornamento. § ‘ Se tutto va a buon fine, esegue il commit della transazione. ContextUtil.SetComplete() ‘ Comunica al client che tutto è andato a buon fine. Return True Catch ‘ Se si è verificato un errore di aggiornamento, annulla la ‘ transazione. ContextUtil.SetAbort() ‘ Comunica al client che si è verificato un problema. Return False Finally ‘ In tutti i casi, chiude la connessione. cn.Close() End Try End Function

Contrassegnando il metodo con l’attributo AutoComplete, la transazione verrà annullata nel caso il metodo sollevi un’eccezione, altrimenti verrà eseguito il commit. In questo caso sarà possibile omettere la chiamata ai metodi SetComplete e SetAbort. Per ulteriori informazioni a riguardo, si rimanda alla documentazione del .NET Framework SDK.

La classe proxy dell’XML Web Service La classe proxy creata da Visual Studio .NET, o dalla utility Wsdl, deriva da System.Web.Protocols.SoapHttp ClientProtocol ed espone metodi aggiuntivi che rispecchiano quelli della classe XML Web service origina-


Capitolo 26 Gli XML Web service

1149

le. La tabella 26-1 elenca i membri principali della classe SoapHttpClientProtocol assieme a una breve descrizione. Tabella 26-1. Le proprietà e i metodi principali della classe SoapHttpClientProtocol. Categoria

Sintassi

Descrizione

Proprietà

AllowAutoRedirect

Se False (il default), il proxy solleva un’eccezione nel caso si tenti un reindirizzamento del server. Se il messaggio contiene informazioni sull’autenticazione o di tipo confidenziale, bisognerebbe impedire il reindirizzamento che altrimenti potrebbe compromettere la sicurezza. La collezione dei certificati del client. Ottiene o imposta una collezione di cookie. Un oggetto ICredential contenente le credenziali utilizzate per l’autenticazione lato client dell’XML Web service. True se tutte le richieste devono contenere credenziali di autenticazione. Se False (il default), le credenziali vengono inviate solo se l’ XML Web non consente accessi anonimi e restituisce un codice di ritorno 401 HTTP quando viene tentata inizialmente la richiesta. Ottiene o imposta informazioni sul server. Questa proprietà consente di invocare l’XML Web service tramite un firewall che dovrebbe impedire chiamate dirette. Un valore enumerato System.Text.Encoding che specifica la codifica da utilizzare quando si sottomettono le richieste (ad esempio, Encoding.UTF8). Il valore predefinito è Nothing, e in questo caso viene utilizzata la codifica predefinita del protocollo sottostante. Il timeout in millisecondi per una richiesta sincrona a un metodo di un XML Web service. L’URL di base dell’XML Web service, ad esempio, http:// www.yourdomain.com/yourservice.asmx. Questa proprietà viene assegnata per reindirizzare la richiesta a un altro XML Web service con la stessa interfaccia di quello che non è attualmente disponibile. L’intestazione dello user agent nella richiesta all’XML Web service. Il default è Mozilla/4.0 (compatible; MSIE 6.0; MS Web Services Client Protocol w.x.yyyy.z), dove w.x.yyyy.z è la versione del Common Language Runtime. Cancella una chiamata sincrona a un metodo di un XML Web service. Questo metodo deve essere invocato da un altro thread; il thread bloccato riceve una WebException. Lega dinamicamente la classe proxy all’XML Web service il cui documento di discovery si trova all’indirizzo contenuto nella proprietà Url. Invoca un metodo in modalità sincrona utilizzando SOAP. Il primo argomento è il nome del metodo, mentre il secondo è una matrice che contiene gli argomenti da passare all’XML Web Service.

ClientCertificates CookieContainer Credentials PreAuthenticate

Proxy

RequestEncoding

Timeout Url

UserAgent

Metodi

Abort

Discover

Metodi (Protected)

Invoke(name, args)

(continua)


1150

Parte VI Applicazioni Internet

Tabella 26-1. Continua Categoria

Sintassi

Descrizione

BeginInvoke(name, args, callback, state) EndInvoke(iasyncres) GetWebResponse (webrequest)

Invoca un metodo in modalità asincrona utilizzando SOAP; restituisce un oggetto IAsyncResult. Termina un metodo asincrono invocato con BeginInvoke. Restituisce la risposta di una richiesta sincrona all’XML Web service.

Chiamate a metodi in late-bound ed early-bound Tutte le classi proxy ereditano dalla classe di base SoapHttpClientProtocol la capacità di invocare i metodi in modalità sincrona e asincrona tramite i metodi protetti Invoke, BeginInvoke ed EndInvoke. Oltre a questi generici metodi di late binding, Visual Studio e la utility Wsdl creano tre metodi fortemente tipizzati nella classe proxy per ciascun metodo della componente XML Web service. Si tratta di metodi pubblici che è possibile invocare dall’applicazione client. Ad esempio, il metodo EuroToDollar del file Converter.asmx fa sì che all’interno della classe proxy vengano creati i metodi che seguono: ‘ Metodo fortemente tipizzato per chiamate asincrone. Public Function DollarToEuro(ByVal amount As Decimal) As Decimal Dim results() As Object = Me.Invoke(“DollarToEuro”, New Object() {amount}) Return CType(results(0),Decimal) End Function ‘ Metodo fortemente tipizzato per avviare le chiamate asincrone Public Function BeginDollarToEuro(ByVal amount As Decimal, _ ByVal callback As AsyncCallback, ByVal asyncState As Object) _ As IAsyncResult Return Me.BeginInvoke(“DollarToEuro”, _ New Object() {amount}, callback, asyncState) End Function ‘ Metodo fortemente tipizzato per terminare le chiamate asincrone. Public Function EndDollarToEuro(ByVal asyncResult As IAsyncResult) As Decimal Dim results() As Object = Me.EndInvoke(asyncResult) Return CType(results(0),Decimal) End Function

Nei paragrafi che seguono, si vedrà come effettuare chiamate sincrone e asincrone a un XML Web service.

Invocare un metodo in modalità sincrona Dal momento che si esegue una chiamata a un XML Web service in esecuzione su un computer in Internet, bisognerebbe sempre proteggere le chiamate asincrone con un timeout. Se si supera il tempo specificato dalla proprietà Timeout, verrà sollevato un oggetto System.Net.WebException, pertanto l’invocazione al metodo andrebbe protetta con un blocco Try…End Try: Dim service As New localhost.SampleService Try ‘ Imposta un timeout di 5 secondi. service.Timeout = 5000 ‘ Invoca un metodo che impiega 10 secondi per giungere a termine. service.LengthyMethodCall(10) Catch ex As WebException If ex.Status = WebExceptionStatus.Timeout Then ‘ L’operazione va in timeout.


Capitolo 26 Gli XML Web service

1151

§ End If Catch ex As Exception ‘ È stata sollevata un’altra eccezione. End Try

Dal momento che il metodo LengthyMethodCall dell’XML Web service SampleService.asmx contiene semplicemente un metodo Thread.Sleep, è possibile utilizzarlo per verificare il comportamento della classe proxy quando la chiamata va in timeout. <WebMethod(Description:=”A lengthy method”)> _ Sub LengthyMethodCall(ByVal seconds As Integer) ‘ Attende un numero specificato di secondi. Thread.Sleep(seconds * 1000) End Sub

La proprietà Timeout può essere utilizzata in concomitanza con la proprietà Url per fornire indirizzi alternativi. Si supponga, ad esempio, di avere un elenco di XML Web service che offrono la stessa funzionalità (e possiedono lo stesso contratto WSDL). A questo punto, è possibile invocare l’XML Web service predefinito – il cui URL viene memorizzato nel WSDL e consumato nel codice sorgente della classe proxy – dopo aver impostato un opportuno timeout. Se il tempo specificato viene superato, è possibile fare in modo che la proprietà Url punti a un XML Web service alternativo e ritentare la chiamata al metodo. La classe proxy espone, inoltre, il metodo Abort che consente di cancellare una chiamata sincrona a un metodo di un XML Web service. Dal momento che una chiamata sincrona blocca il thread, è possibile invocare il metodo Abort solo da un altro thread, quindi questa tecnica ha senso nel caso l’applicazione possieda almeno due thread. (Se si crea un thread aggiuntivo soltanto per invocare il metodo Abort, sarebbe bene valutare la possibilità di invocarlo, invece, in modalità asincrona). Se si elimina una chiamata sincrona, il thread corrispondente riceverà un oggetto WebException. Il metodo Abort consente di cancellare una chiamata sincrona a un metodo a seconda di fattori che non dipendono dalla scadenza del timeout, come nell’esempio che segue: Dim service As New localhost.SampleService Sub RunTheMethod() ‘ Esegue un altro thread. Dim tr As New Thread(AddressOf CallWebMethod) tr.Start() ‘ Annulla la chiamata al metodo quando una condizione diventa vera, ad ‘ esempio quando un file viene aggiornato o un processo giunge a termine. ‘ ... ‘ (In questo esempio si attende solo qualche secondo). Thread.Sleep(5000) service.Abort() End Sub Sub CallWebMethod() Try ‘ Invoca un metodo che impiega 10 secondi per giungere a termine. service.LengthyMethodCall(10) Catch ex As WebException If ex.Status = WebExceptionStatus.RequestCanceled Then ‘ La chiamata al metodo è stata cancellata. § End If End Try End Sub


1152

Parte VI Applicazioni Internet

Invocare un metodo in modalità asincrona Come ho mostrato in precedenza, la classe proxy espone una coppia di metodi Beginxxxx ed Endxxxx per ogni procedura della componente XML Web service contrassegnata con l’attributo WebMethod. Questi due metodi possono essere utilizzati per invocare l’XML Web service in modalità asincrona. A questo proposito, le azioni necessarie sono identiche a quelle che occorrono per invocare un delegate sempre in modalità asincrona. Dal momento che i delegate sono già stati trattati nel capitolo 13, in questo contesto mi limiterò a fornire un esempio di chiamata asincrona a un metodo di un XML Web service: Sub CallAsyncMethod() Dim service As New localhost.SampleService ‘ Invoca un metodo lento che impiega circa 5 secondi per giungere a ‘ termine. Si noti che l’oggetto proxy viene passato nel terzo argomento. Dim ar As IAsyncResult = service.BeginLengthyMethodCall(5, _ AddressOf MethodCallback, service) End Sub ‘ Questo è il metodo di callback. Sub MethodCallback(ByVal ar As IAsyncResult) ‘ Estrae l’oggetto proxy dalla proprietà AsyncState. Dim service As localhost.SampleService = _ DirectCast(ar.AsyncState, localhost.SampleService) ‘ Completa la chiamata al metodo. service.EndLengthyMethodCall(ar) End Sub

Annullare una chiamata asincrona a un XML Web service è leggermente più complesso rispetto al caso di una chiamata sincrona. È necessario, infatti, convertire l’oggetto IASyncResult restituito dal metodo Beginxxxx in un oggetto WebClientAsyncResult e quindi invocare il metodo Abort di quest’ultimo: Sub AbortAsyncMethodCall() Dim service As New localhost.SampleService() ‘ Esegue un metodo che impiega 10 secondi per giungere termine. Dim ar As IAsyncResult = service.BeginLengthyMethodCall(5, _ Nothing, Nothing) ‘ In questo punto eseguire qualcos’altro. § If ar.IsCompleted Then ‘ Se il metodo è giunto a termine, completa la chiamata. service.EndLengthyMethodCall(ar) Else ‘ Altrimenti annulla la chiamata al metodo. Dim wcar As WebClientAsyncResult = CType(ar, WebClientAsyncResult) wcar.Abort() End If End Sub

Metodi unidirezionali Un caso particolare di chiamata asincrona si verifica quando al client non interessa effettivamente il valore restituito dall’XML Web service. Ad esempio, si potrebbe realizzare un XML Web service che espone uno o più metodi che i client invocano solo per segnalare che si è verificato qualcosa o per eseguire dei comandi (tipo lanciare la compilazione di un batch molto lungo). In situazioni di questo tipo, è possibile raggiungere l’asincronia perfetta contrassegnando il metodo dell’XML Web service utilizzando l’attributo SoapDocumentMethod con il relativo argomento OneWay impostato a True: <WebMethod(), SoapDocumentMethod(OneWay:=True)> _ Sub OneWayLengthyMethodCall(ByVal seconds As Integer)


Capitolo 26 Gli XML Web service

1153

‘ Simula un metodo che impiega molto tempo. Thread.Sleep(seconds * 1000) End Sub

I metodi così contrassegnati non devono possedere né un valore di ritorno né argomenti ByRef. Oltre a ciò, tali metodi non possono accedere ai relativi oggetti HttpContext, e ogni proprietà della classe XML Web service restituisce Nothing.

La proprietà CookieContainer Come si è visto nel paragrafo “Abilitare lo stato della sessione” di questo capitolo, la classe proxy standard non funziona bene con i metodi degli XML Web service che si basano sullo stato della sessione – ossia, quelli per cui l’attributo EnableSession è impostato a True – in quanto tale classe non è in grado di fungere da contenitore per i cookie. La classe proxy, pertanto, non può memorizzare il cookie della sessione che ASP.NET invia al client quando viene individuata una nuova sessione. Fortunatamente, per rendere la classe proxy un contenitore adatto ai cookie basta verificare che la proprietà CookieContainer contenga un riferimento a un oggetto System.Net.CookieContainer: Dim service As New localhost.SampleService() Sub TestIncrementCounterMethod() ‘ Rende l’oggetto proxy un contenitore di cookie, se necessario. If service.CookieContainer Is Nothing Then service.CookieContainer = New CookieContainer() End If ‘ Ogni volta che questo metodo viene invocato, il valore del controllo ‘ Label viene incrementato di 1. Label1.Text = service.IncrementCounter() End Sub

Dopo aver impostato la proprietà CookieContainer, l’oggetto proxy è in grado di memorizzare il cookie della sessione. La sessione termina quando l’oggetto proxy viene impostato a Nothing oppure quando il timeout della sessione scade senza invocare alcun metodo dell’XML Web service. (Il timeout predefinito per la sessione è di 20 minuti). Dal momento che la proprietà CookieContainer restituisce una collezione di cookie, è possibile leggere quelli che contiene e aggiungerne di propri. In particolare, è possibile leggere uno speciale cookie della sessione denominato ASP.NET_SessionId, salvarlo in una variabile su disco e aggiungerlo alla collezione di cookie in una successiva chiamata: Dim saveCookie As System.Net.Cookie Sub CallStatefulWebServiceMethod() Dim service As New localhost.SampleService() ‘ Rende l’oggetto proxy un contenitore per i cookie. service.CookieContainer = New CookieContainer() ‘ Se esiste già un cookie, lo aggiunge alla collezione di cookie. If Not (saveCookie Is Nothing) Then service.CookieContainer.Add(saveCookie) End If ‘ Invoca il metodo dell’XML Web service. Label1.Text = service.IncrementCounter() ‘ Salva il cookie se si tratta della prima chiamata. If saveCookie Is Nothing Then ‘ Sostituisce l’argomento con l’URL dell’XML Web service. Dim cookieUri As New Uri(“http://localhost”) ‘ Salva il cookie ASP.NET_SessionID che appartiene all’URI localhost. saveCookie = service.CookieContainer.GetCookies(cookieUri).Item _


1154

Parte VI Applicazioni Internet

(“ASP.NET_SessionId”) End If End Sub

Questa tecnica può essere applicata a due situazioni importanti. Innanzitutto, due applicazioni Windows Form possono condividere la stessa sessione, indipendentemente dal fatto che siano in esecuzione sulla stessa macchina o su macchine diverse sempre che esista un mezzo per scambiare il contenuto del cookie della sessione. In secondo luogo, tutte le pagine di un’applicazione Web Form ASP.NET possono invocare i metodi di un XML Web service e condividere lo stesso insieme di variabili Session. (In questo caso, le singole pagine .aspx dovrebbero salvare il cookie in una variabile di tipo Session o Application).

Le eccezioni SOAP I metodi degli XML Web service sono in grado di sollevare eccezioni, sia direttamente tramite un’istruzione Throw, sia indirettamente nel caso eseguano un’operazione non ammessa. Quando si interagisce con un XML Web service, è necessario tenere in considerazione altri tipi di errori, come quelli generati da un client che utilizza una versione obsoleta del contratto WSDL (e della classe proxy). Ogni volta che si verifica un errore mentre un XML Web service elabora una richiesta, il messaggio SOAP restituito al client contiene un blocco <soap:Fault> all’interno del corpo invece dell’usuale blocco <soap:Body>. Dal punto di vista del client, questo blocco fault viene trasformato in un oggetto SoapException, il quale rappresenta il tipo da cercare nella clausola Catch del blocco Try…End Try. La classe SoapException eredita da SystemException tutte le proprietà usuali, alle quali ne aggiunge alcune specifiche. I membri più significativi della classe SoapException sono: ■ Message Il messaggio dell’eccezione originale. ■ Actor L’URL dell’XML Web service che solleva l’eccezione. ■ Code Un oggetto XmlQualifiedName, il quale specifica il codice di errore SOAP che descrive la causa generale dell’errore. ■ Detail Un oggetto di tipo XmlNode che rappresenta informazioni sugli errori specifici per l’applicazione. Questa proprietà viene impostata solo se l’errore si è verificato quando l’XML Web service sta elaborando il corpo del messaggio, ed è Nothing in tutti gli altri casi (come quando il problema riguarda l’intestazione o il formato del messaggio). Nella maggior parte dei casi, le proprietà sulle quali bisognerebbe rivolgere l’attenzione sono Message e Code. Un piccolo problema è dovuto al fatto che la stringa d’errore originale è nascosta all’interno del valore restituito dalla proprietà Message. Ad esempio, se il metodo dell’XML Web service solleva una NullReferenceException, questa è la stringa che verrà trovata nella proprietà Message: System.Web.Services.Protocols.SoapException: Server was unable to process request. —> System.NullReferenceException: Object reference not set to an instance of an object. at MoneyConverter.SampleService.ThrowAnException() — End of inner exception stack trace —

Il modo più semplice per estrarre il nome dell’eccezione vera e propria consiste nell’utilizzare le regular expression. Quella che segue è una routine riutilizzabile che esegue proprio questa operazione: ‘ Questo codice assume l’utilizzo della seguente istruzione Imports: ‘ Imports System.Text.RegularExpression ‘ Estrae il nome dell’eccezione “interna”. Function GetWSException(ByVal ex As SoapException) As String ‘ Analizza la proprietà Message dell’eccezione. Dim mc As MatchCollection = Regex.Matches(ex.Message, “—> ([^:]+):”) If mc.Count >= 1 Then


Capitolo 26 Gli XML Web service

1155

‘ È stata trovata una corrispondenza – il primo gruppo contiene il ‘ valore. Return mc.Item(0).Groups(1).Value End If End Function

È possibile utilizzare la funzione GetWSException all’interno di un blocco Try…End Try come segue: Try Dim service As New localhost.SampleService() service.ThrowAnException() Catch ex As SoapException _ When GetWSException(ex) = “System.NullReferenceException” ‘ Un’eccezione per un riferimento nullo § Catch ex As SoapException _ When GetWSException(ex) = “System.DivideByZeroException” ‘ Un’eccezione per una divisione per zero § End Try

La proprietà Code della classe SoapException è utile in quanto consente di classificare il tipo di eccezione ricevuta. Questa proprietà restituisce un oggetto XmlQualifiedName, la cui proprietà Name può assumere uno dei seguenti valori: ■ VersionMismatch È stato trovato un namespace non valido. ■ MustUnderstand Il client ha inviato un elemento il cui attributo MustUnderstand è 1, ma il server non è in grado di elaborarlo. ■ Client La richiesta del client non è formattata in modo opportuno oppure non contiene le informazioni appropriate. Questa eccezione indica che il messaggio non dovrebbe essere inviato nuovamente se non si apporta alcuna modifica. ■ Server Si è verificato un errore sul server ma non è stato provocato dal contenuto del messaggio; si tratta della proprietà Code restituita quando il codice in esecuzione all’interno dell’XML Web service solleva un’eccezione. Per agevolare la verifica della proprietà Code, la classe SoapException definisce quattro costanti denominate VersionMismatchFaultCode, MustUnderstandFaultCode, ClientFaultCode e ServerFaultCode. Il codice che segue utilizza tali costanti per determinare il tipo di errore che si è verificato, utilizzando la clausola When del blocco Try…End Try: Try Dim service As New localhost.SampleService() service.ThrowAnException() Catch ex As SoapException _ When ex.Code.Equals(SoapException.VersionMismatchFaultCode) ‘ È stato utilizzato un namespace non ammesso. § Catch ex As SoapException _ When ex.Code.Equals(SoapException.ServerFaultCode) ‘ È stato utilizzato un namespace non ammesso. § End Try

Argomenti avanzati Nell’ultima parte di questo capitolo, verranno trattati tre argomenti avanzati che possono rivelarsi utili nell’implementazione di XML Web service reali: le intestazioni SOAP, la sicurezza e le estensioni SOAP.


1156

Parte VI Applicazioni Internet

Le intestazioni SOAP Come si è visto nel paragrafo “Il protocollo SOAP” nella prima parte di questo capitolo, un messaggio SOAP può contenere informazioni aggiuntive all’interno della propria intestazione. L’intestazione del messaggio, tuttavia, è opzionale ed è questo il motivo per cui tutti gli esempi visti fino a questo punto ne erano sprovvisti. Ho creato un esempio che mostra come passare un’intestazione SOAP e in che modo un XML Web service possa servirsene per trasformare date e informazioni di altro tipo nel formato atteso dal client. Per creare una classe XML Web service che funzioni con un’intestazione SOAP, è necessario: 1. Definire una classe che contenga i campi che si vogliono passare tramite l’intestazione. Questa classe deve derivare da System.Web.Services.Protocols.SoapHeader. 2. Definire un campo pubblico nella classe che ha lo stesso tipo di quella definita al punto precedente. 3. Aggiungere un attributo SoapHeader a tutti i metodi ai quali si vuole far leggere l’intestazione SOAP. L’argomento di tale attributo è il nome del campo definito al punto precedente. Quello che segue è un esempio di XML Web service che espone un metodo denominato GetClientTime, il quale restituisce l’ora locale del client formattata secondo la lingua del client. Le informazioni sulla lingua e il numero di fusi orari dal Coordinated Universal Time (UTC) vengono passati in un’istanza della classe UserInfoHeader: Public Class SampleService Inherits System.Web.Services.WebService ‘ Questa è la variabile pubblica che riceve l’intestazione userInfo. Public userInfo As UserInfoHeader <WebMethod(), SoapHeader(“userInfo”)> _ Function GetClientTime() As String ‘ L’ora locale del server espresso in Coordinated Universal Time. Dim serverTime As Date = Date.Now.ToUniversalTime ‘ Converte nel fuso orario del client. Dim clientTime As Date = serverTime.AddHours(userInfo.TimeOffset) ‘ Crea un oggetto CultureInfo con informazioni appropriate sulla ‘ lingua. Dim ci As New System.Globalization.CultureInfo(userInfo.Culture) ‘ Restituisce l’ora formattata applicando le regole del client. Return clientTime.ToString(ci) End Function End Class ‘ Questa classe definisce le informazioni contenute nell’intestazione SOAP. Public Class UserInfoHeader Inherits SoapHeader ‘ Questo membro contiene il nome della lingua dell’utente. Public Culture As String = “” ‘ Questo membro contiene la differenza rispetto all’ora UTC. Public TimeOffset As Single = 0 End Class

Quando si crea il contratto WSDL e lo si utilizza per realizzare la classe proxy, viene generata anche la definizione del client per la classe UserInfoHeader, in modo che il codice client possa creare un’istanza di tale classe e inizializzarla all’occorrenza. Oltre a ciò, la classe proxy viene estesa con un campo denominato headerclassValue – UserInfoHeaderValue, in questo esempio – al quale il client può assegnare l’oggetto UserInfoHeader:


Capitolo 26 Gli XML Web service

1157

‘ Come invocare un metodo di un XML Web service che accetta ‘ un’intestazione SOAP ‘ Crea un’intestazione. Dim userInfo As New localhost.UserInfoHeader() ‘ Questo è l’identificatore per la lingua italiana. userInfo.Culture = “it-it” ‘ L’orario in Italia è un’ora avanti rispetto a quella di Greenwich. userInfo.TimeOffset = 1 ‘ Crea un’istanza della classe proxy. Dim service As New localhost.SampleService() ‘ Assegna l’intestazione al campo particolare xxxxValue. service.UserInfoHeaderValue = userInfo ‘ Invoca l’XML Web service. Dim res As String = service.GetClientTime()

L’attributo SoapHeader rende l’intestazione obbligatoria e il client ottiene un’eccezione nel caso nessuna intestazione risulti associata all’oggetto proxy; è comunque possibile rendere l’intestazione opzionale aggiungendo un argomento Required impostato a False. In questo caso, sarà necessario garantire il buon funzionamento del codice all’interno del metodo dell’XML Web service anche se il client non invia un’intestazione: <WebMethod(), SoapHeader(“userInfo”, Required:=False)> _ Function GetClientTime() As String ‘ Fornisce un oggetto userInfo predefinito se il client l’ha omesso. If userInfo Is Nothing Then userInfo = New UserInfoHeader() ‘ In questo caso, al client viene restituita l’ora UTC. End If ‘ ...(Il resto del metodo rimane invariato)... § End Function

Ancora due parole sulle intestazioni SOAP: queste vengono inviate all’XML Web service ma non vengono restituite al client, per risparmiare larghezza di banda. In altre parole, l’oggetto header funge da argomento di solo input per il metodo dell’XML Web service. Tuttavia, questo comportamento non è obbligatorio ed è possibile controllare in modo preciso le modalità di invio e ricezione delle intestazioni. La chiave di tale funzionalità è l’argomento Direction dell’attributo SoapHeader: il valore predefinito è In, ma può assumere anche i valori InOut o Out: <WebMethod(), SoapHeader(“userInfo”, Required:=False, _ Direction:=SoapHeaderDirection.InOut)> _ Function GetClientTime() As String ‘ Fornisce un oggetto userInfo predefinito se il client l’ha omesso. If userInfo Is Nothing Then ‘ (Questa intestazione viene restituita al client). userInfo = New UserInfoHeader() userInfo.Culture = “EN-US” ‘ Inglese americano userInfo.TimeOffset = -5 ‘ Fuso orario dell’America orientale End If ‘ ...(Il resto del metodo rimane invariato)... § End Function

Nel paragrafo “Autenticazione personalizzata” più avanti in questo capitolo verranno mostrati altri utilizzi delle intestazioni SOAP.


1158

Parte VI Applicazioni Internet

La sicurezza degli XML Web service La sicurezza degli XML Web service non differisce in modo sostanziale dalla sicurezza standard di ASP.NET, e possono essere applicati anche tutti i meccanismi di sicurezza illustrati nel capitolo 24. La particolarità della sicurezza degli XML Web service risiede nella loro natura non interattiva. Ad esempio, se si disabilitano gli accessi anonimi per un file .asmx all’interno di IIS, ogni richiesta del client per tali file fallisce senza però mostrare alcuna finestra di dialogo. Un’altra conseguenza della natura programmatica degli XML Web service: è possibile utilizzare l’autenticazione tramite form e reindirizzare tutte le richieste non autenticate a un determinato URL (utilizzando l’elemento LoginUrl del file web.config), ma questa tattica non ha alcun utilizzo pratico in quanto il client dell’XML Web service riceverebbe HTML invece del messaggio SOAP che sta aspettando. Sebbene sia possibile eliminare questo ostacolo fornendo una funzione personalizzata che crea manualmente il cookie di autenticazione, in generale l’autenticazione tramite form non rappresenta un metodo adatto per gli XML Web service, e pertanto non verrà descritta in questo libro.

L’autenticazione di Windows L’autenticazione di Windows rappresenta il metodo di autenticazione più semplice utilizzabile con gli XML Web service e non può accettare richieste anonime. Indipendentemente dal fatto che le pagine siano state protette con l’autenticazione di Windows di tipo Basic, Digest o Integrated, è possibile fornire le credenziali a IIS assegnando un oggetto NetworkCredential alla proprietà Credential dell’oggetto proxy, come nel seguente frammento di codice: ‘ Crea un oggetto System.Net.NetworkCredential. Dim nc As New NetworkCredential() nc.UserName = “username” nc.Password = “userpwd” nc.Domain = “domainname” ‘ Crea l’oggetto proxy e gli assegna le credenziali dell’utente. Dim service As New localhost.Converter() service.Credentials = nc ‘ Effettua la chiamata. Dim euros As Decimal = service.DollarToEuro(100)

Se la pagina .asmx richiede l’autenticazione di Windows e vengono passate credenziali non valide (oppure nessuna credenziale), l’applicazione client riceve un oggetto WebException la cui proprietà Message è: The request failed with HTTP status 401: Access Denied.

Se l’applicazione client è dotata di interfaccia utente, si potrebbe intercettare l’eccezione, mostrare una casella di messaggio e chiedere all’utente di immettere username e password e di effettuare un secondo tentativo. Tutto quanto è stato detto nel capitolo 24 riguardo i diversi metodi di autenticazione di Windows vale anche nel caso degli XML Web service. Ad esempio, quando si utilizza l’autenticazione Basic, lo username e la password vengono inviati in chiaro (in formato codificato Base64), quindi bisognerebbe utilizzare la sicurezza integrata di Windows, se possibile.

Autenticazione personalizzata Come accade nel caso di applicazioni ASP.NET standard, l’autenticazione di Windows presenta un inconveniente piuttosto pesante: spesso non si desidera creare un account di Windows per ogni utente che utilizza l’XML Web service. In questo paragrafo, mostrerò come sfruttare le intestazioni SOAP per implementare un meccanismo di autenticazione personalizzato.


Capitolo 26 Gli XML Web service

1159

L’idea è semplice: ogni chiamata proveniente dall’applicazione client deve includere un’intestazione SOAP contenente username e password. Tutti i metodi degli XML Web service che richiedono l’autenticazione – ad esempio quelli che necessitano di un qualche tipo di sottoscrizione – invocano una funzione principale, denominata ValidateUser, che controlla le credenziali contenute nell’intestazione e solleva un’eccezione se queste non sono valide o se la sottoscrizione è scaduta. Quello che segue è il codice sorgente di una classe XML Web service che contiene un metodo di questo tipo: Public Class SampleService Inherits System.Web.Services.WebService Public accountInfo As AccountInfoHeader <WebMethod(), SoapHeader(“accountInfo”)> _ Function ProtectedMethod() As Boolean ‘ Verifica che le credenziali siano corrette, altrimenti solleva ‘ un’eccezione. ValidateAccount() ‘ Restituisce un valore. ‘ (Un’applicazione reale dovrebbe eseguire qualcosa di più utile ‘ di questo). Return True End Function ‘ Convalida username e password (funzione privata). Private Sub ValidateAccount() ‘ Solleva un’eccezione se manca l’intestazione. If accountInfo Is Nothing Then Throw New SoapException(“Missing user info header”, _ SoapException.ClientFaultCode) End If ‘ Solleva un’eccezione se i membri dell’intestazione non sono ‘ impostati. If accountInfo.UserName = “” Or accountInfo.Password = “” Then Throw New SoapException(“Missing user info”, _ SoapException.ClientFaultCode) End If ‘ Solleva un’eccezione se l’utente non è valido. If Not CheckUser(accountInfo.UserName, accountInfo.Password) Then Throw New SoapException(“Insufficient subscription level”, _ SoapException.ClientFaultCode) End If ‘ Esce regolarmente se tutto è corretto. End Sub ‘ Verifica che le credenziali dell’utente siano valide. Function CheckUser(ByVal username As String, ByVal password As String) _ As Boolean ‘ (Un’applicazione reale utilizzerebbe un database). If username = “JoeDoe” And password = “jdpwd” Then Return True ElseIf username = “AnnSmith” And password = “aspwd” Then Return True Else ‘ Utente sconosciuto o credenziali non valide. Return False End If End Function End Class


1160

Parte VI Applicazioni Internet

‘ Questa è la classe SOAP Header. Public Class AccountInfoHeader Inherits SoapHeader Public UserName As String Public Password As String End Class

Il codice lato client assomiglia a quello visto in precedenza nel paragrafo “Le intestazioni SOAP” di questo capitolo: Private Sub CallProtectedMethod() ‘ Crea l’intestazione SOAP con informazioni sull’account. Dim accountInfo As New localhost.AccountInfoHeader() accountInfo.UserName = “JoeDoe” accountInfo.Password = “jdpwd” ‘ Passe le informazioni sull’account alla classe proxy. Dim service As New localhost.SampleService() service.AccountInfoHeaderValue = accountInfo ‘ Invoca il metodo protetto dell’XML Web service. Dim res As Boolean = service.ProtectedMethod() End Sub

Il vantaggio di questo approccio è la possibilità di espandere facilmente la procedura ValidateUser della classe XML Web service per implementare sofisticati criteri di autorizzazione. Ad esempio, la procedura può utilizzare tecniche di reflection per estrarre il nome del metodo che l’ha invocata – ossia, il metodo dell’XML Web service invocato dal client – e garantire che gli utenti possiedano un livello di sottoscrizione che consenta loro di effettuare la chiamata. Ecco un’implementazione di tale concetto: ‘ Convalida username e password. (Procedura privata) Private Sub ValidateAccount() ‘ Solleva un’eccezione se manca l’intestazione. If accountInfo Is Nothing Then Throw New SoapException(“Missing user info header”, _ SoapException.ClientFaultCode) End If ‘ Solleva un’eccezione se i membri dell’intestazione non sono ‘ impostati. If accountInfo.UserName = “” Or accountInfo.Password = “” Then Throw New SoapException(“Missing user info”, _ SoapException.ClientFaultCode) End If ‘ Estrae il livello di sottoscrizione di questo utente. Dim thisUserSubscriptionLevel As Integer = GetUserSubscriptionLevel _ (accountInfo.UserName, accountInfo.Password) ‘ Esce se le credenziali non sono valide. If thisUserSubscriptionLevel < 0 Then Throw New SoapException(“Unknown user”, _ SoapException.ClientFaultCode) End If ‘ Estrae il nome del metodo che ha invocato la procedura. Dim st As New System.Diagnostics.StackTrace(False) ‘ GetFrame(0) descrive la procedura in esecuzione, ‘ GetFrame(1) descrive la procedura chiamante. Dim sf As System.Diagnostics.StackFrame = st.GetFrame(1) Dim mb As System.Reflection.MethodBase = sf.GetMethod ‘ Estrae il livello di sottoscrizione richiesto per invocare ‘ il metodo.


Capitolo 26 Gli XML Web service

1161

Dim requiredSubscriptionLevel As Integer Select Case mb.Name Case “ProtectedMethod” requiredSubscriptionLevel = 1 Case “AnotherProtectedMethod” requiredSubscriptionLevel = 2 End Select ‘ Solleva un’eccezione se il livello di sottoscrizione non è ‘ sufficiente. If thisUserSubscriptionLevel < requiredSubscriptionLevel Then Throw New SoapException(“Insufficient subscription level”, _ SoapException.ClientFaultCode) End If ‘ Esce regolarmente se tutto è corretto. End Sub ‘ Ottiene il livello di sottoscrizione dell’utente, oppure -1 se le ‘ credenziali non sono valide. Function GetUserSubscriptionLevel(ByVal username As String, _ ByVal password As String) As Integer ‘ (Un’applicazione reale utilizzerebbe un database). If username = “JoeDoe” And password = “jdpwd” Then Return 1 ElseIf username = “AnnSmith” And password = “aspwd” Then Return 2 Else ‘ Utente sconosciuto o credenziali non valide. Return -1 End If End Function

Ora esiste un posto centralizzato nel quale è possibile controllare tutti gli accessi agli XML Web service, pertanto è possibile modificare la sottoscrizione e applicare la policy senza apportare modifiche al codice di qualsiasi altro metodo dell’XML Web service. È possibile migliorare ulteriormente questo meccanismo creando un’estensione SOAP che legga l’intestazione SOAP ed effettui l’autenticazione e l’autorizzazione prima che il flusso dell’esecuzione abbia raggiunto il metodo, come si vedrà nel prossimo paragrafo.

Le estensioni SOAP L’architettura degli XML Web service può essere completamente ridefinita dal programmatore, se necessario, per mezzo delle estensioni SOAP. Un’estensione SOAP è un modulo software in grado di eseguire codice personalizzato prima che il messaggio SOAP raggiunga il metodo dell’XML Web service. Più precisamente, esistono quattro punti nei quali un’estensione SOAP ha la possibilità di eseguire codice personalizzato. Questi corrispondono ai quattro stati nei quali si può trovare un messaggio SOAP. (Si veda la parte di destra della figura 26-13). ■ BeforeDeserialize Il messaggio SOAP è stato ricevuto dal client, si trova sempre in formato XML e non è ancora stato deserializzato in un oggetto in memoria. ■ AfterDeserialize Il messaggio SOAP è stato serializzato in un oggetto e il metodo dell’XML Web service sta per essere invocato. ■ BeforeSerialize Il metodo dell’XML Web service ha terminato l’esecuzione e l’oggetto risultato sta per essere serializzato in XML. ■ AfterSerialize L’oggetto risultato è stato serializzato in un messaggio SOAP e il testo XML sta per essere inviato al client.


1162

Parte VI Applicazioni Internet

La cosa interessante è che un messaggio SOAP che lascia un client – ossia, l’oggetto proxy sul client – subisce gli stessi quattro stati, ma in ordine diverso. (Si veda la metà di sinistra della figura 26-13).

Figura 26-13. I quattro stati che un messaggio SOAP attraversa sul server e sul client.

Un’estensione SOAP può leggere i dati XML che vengono inviati dal client al server e viceversa e, se necessario, modificarli. Così, ad esempio, si potrebbe salvare l’XML per motivi di registrazione o di debugging oppure criptare i valori di ritorno per evitare ascolti indesiderati e manomissioni, anche se quest’ultima operazione richiederebbe un’estensione SOAP in esecuzione sul client per decriptare il risultato. Dal punto di vista di uno sviluppatore, un’estensione SOAP non è altro che una classe che deriva dalla classe astratta System.Web.Services.Protocols.SoapExtension e ne ridefinisce alcuni metodi. Tuttavia, prima di realizzare questa classe, bisognerebbe vedere come associare un’estensione SOAP a un XML Web service. A questo proposito i metodi da utilizzare sono due: ■ Aggiungere una voce al file di configurazione. (In questo caso, l’estensione SOAP viene invocata per tutti i metodi associati all’attributo WebMethod). ■ Definire un attributo personalizzato e utilizzarlo per contrassegnare solo i metodi per i quali si vuole attivare l’estensione SOAP. Questo metodo risulta più flessibile in quanto è possibile passare uno o più argomenti al costruttore dell’attributo personalizzato e rendere questi disponibili alla classe dell’estensione SOAP. (Questo meccanismo potrebbe essere utilizzato, ad esempio, per associare una tariffa a ogni metodo di un XML Web service). Ecco come implementare questi due metodi di attivazione.

Attivare un’estensione SOAP da un file di configurazione È possibile installare un’estensione SOAP aggiungendo un tag <add> nel blocco <soapExtensionTypes> del file web.config: <configuration> <system.web> <webServices> <soapExtensionTypes> <add type=”Logger.LoggerExtension, logger” Priority=”1" Group=”0" /> <soapExtensionTypes> <webServices> <system.web> <configuration>

dove type è il nome della classe dell’estensione SOAP, Group è il gruppo dell’estensione SOAP (può essere 0 o 1) e Priority è la priorità all’interno del gruppo (0 è la priorità più alta). Un’estensione SOAP può appartenere a uno dei tre gruppi:


Capitolo 26 Gli XML Web service

1163

Group 0 Le estensioni SOAP di questo gruppo hanno la priorità maggiore e possono elaborare il messaggio prima di quelle che appartengono agli altri due gruppi. ■ Medium group Estensioni SOAP attivate tramite attributi personalizzati applicati ai metodi della classe XML Web service. (Nel prossimo paragrafo sarà possibile vedere come attivare queste estensioni). ■ Group 1 Le estensioni SOAP di questo gruppo hanno la priorità minore ed elaborano il messaggio dopo quelle che appartengono ai primi due gruppi. Attivare un’estensione SOAP modificando la classe web.config rappresenta la soluzione migliore quando questa deve essere applicata a tutti i metodi dell’XML Web service e quando non bisogna passarle alcun argomento. Ad esempio, si potrebbe utilizzare questa tecnica per attivare un’estensione SOAP che tenga traccia di tutte le richieste in input o che cripti il risultato di tutti i metodi dell’XML Web service. ■

Attivare un’estensione SOAP con un attributo personalizzato Il secondo, e più flessibile, metodo per attivare un’estensione SOAP consiste nell’utilizzare un attributo personalizzato per contrassegnare i metodi dell’XML Web service per i quali deve essere utilizzata tale estensione. Questa tecnica consente di applicare l’estensione solo a un sottoinsieme di tutti i metodi esistenti e di definire gli argomenti che vengono passati all’estensione SOAP metodo per metodo. Nei paragrafi che seguono, sarà possibile vedere come implementare un’estensione SOAP che monitorizzi gli accessi a ogni metodo dell’XML Web service contrassegnato con l’attributo SoapCustomAuthenticationAttribute. Il costruttore di questo attributo accetta un intero che definisce il livello di sottoscrizione minimo richiesto perché un client possa invocare il metodo contrassegnato con tale attributo. (Questa è la versione dell’estensione SOAP del metodo di autenticazione e autorizzazione descritto in precedenza nel paragrafo “Autenticazione personalizzata” di questo capitolo). Ad esempio, un metodo di SampleService.asmx che può essere invocato solo dai client il cui livello di sottoscrizione è 2 o superiore dovrebbe essere: <WebMethod(), SoapCustomAuthentication(2), SoapHeader(“accountInfo”)> _ Function YetAnotherProtectedMethod() As Boolean § End Function

La classe SoapCustomAuthenticationAttribute deve ereditare dalla classe astratta SoapExtensionAttribute. Tutte le classi che derivano da questa classe astratta devono ridefinire le proprietà Priority ed ExtensionType. Quest’ultima è particolarmente importante in quanto restituisce l’oggetto System.Type che definisce la classe dell’estensione SOAP (denominata SoapExtensionAuthentication, in questo caso). Nell’esempio che segue, la classe dell’attributo espone un’ulteriore proprietà, RequiredSubscriptionLevel, che definisce il livello di sottoscrizione minimo necessario a invocare il metodo al quale viene applicato l’attributo. Il livello di sottoscrizione minimo è l’unico argomento che occorre passare al costruttore dell’attributo: ‘ L’attributo personalizzato che occorre utilizzare per contrassegnare i ‘ metodi che richiedono un particolare livello di sottoscrizione. <AttributeUsage(AttributeTargets.Method)> _ Public Class SoapCustomAuthenticationAttribute Inherits SoapExtensionAttribute ‘ Il costruttore dell’attributo Sub New(ByVal requiredSubscriptionLevel As Integer) Me.RequiredSubscriptionLevel = requiredSubscriptionLevel End Sub ‘ È necessario ridefinire la proprietà Priority.


1164

Parte VI Applicazioni Internet

Dim m_Priority As Integer Public Overrides Property Priority() As Integer Get Return m_Priority End Get Set(ByVal Value As Integer) m_Priority = Value End Set End Property ‘ La proprietà ExtensionType restituisce il tipo della classe ‘ SoapExtension. Public Overrides ReadOnly Property ExtensionType() As System.Type Get Return GetType(SoapCustomAuthentication) End Get End Property ‘ RequiredSubscriptionLevel è una proprietà personalizzata per ‘ questo attributo. Dim m_RequiredSubscriptionLevel As Integer Property RequiredSubscriptionLevel() As Integer Get Return m_RequiredSubscriptionLevel End Get Set(ByVal Value As Integer) m_RequiredSubscriptionLevel = Value End Set End Property End Class

La classe dell’estensione SOAP L’estensione SOAP vera e propria è contenuta in una classe che deriva dalla classe astratta System.Web.Services.Protocols.SoapExtension e ne ridefinisce alcuni membri. Dal momento che la struttura di questa classe risulta piuttosto complessa, mostrerò e commenterò il relativo listato un metodo alla volta, seguendo l’ordine con il quale ciascuno viene invocato dall’infrastruttura di ASP.NET. Il metodo GetInitializer viene invocato solo una volta durante il ciclo di vita dell’estensione SOAP. Esistono due versioni di overload di tale metodo, a seconda del fatto che l’estensione venga attivata tramite una voce del file di configurazione o tramite un attributo personalizzato nella classe dell’XML Web service. Nella prima istanza, il metodo riceve un oggetto System.Type che corrisponde al tipo della classe dell’XML Web service. (Questo argomento è necessario in quanto un’estensione SOAP può servire più classi di un XML Web service). Nell’ultima istanza, il metodo GetInitializer riceve un riferimento all’attributo personalizzato e un oggetto MethodInfo che descrive il metodo al quale l’attributo deve essere applicato. In entrambi i casi, il metodo GetInitializer si suppone che restituisca un valore Object. Tale valore viene quindi passato come argomento al metodo Initialize. Questo approccio della doppia inizializzazione rende il lavoro dello sviluppatore un po’ più complicato ma migliora le prestazioni. In questo esempio specifico, il metodo GetInitilizer restituisce la proprietà RequiredSubscriptionLevel dell’attributo personalizzato oppure il valore 1 se l’estensione SOAP è stata attivata tramite una voce del file di configurazione: ‘ La classe Soap extension. Class SoapCustomAuthentication Inherits SoapExtension


Capitolo 26 Gli XML Web service

1165

‘ Questo metodo di overloading di GetInitializer viene invocato se ‘ l’estensione SOAP è installata in web.config; riceve il tipo della ‘ classe WebService. Public Overloads Overrides Function GetInitializer( _ ByVal serviceType As System.Type) As Object ‘ In questo caso, viene restituito semplicemente 1, il più basso ‘ livello di sottoscrizione richiesto per accedere al metodo dell’XML ‘ Web service. If serviceType Is GetType(SampleService) Then Return 1 End If End Function ‘ ‘ ‘ ‘

Questo metodo di overloading di GetInitializer viene invocato se l’estensione SOAP è installata in quanto un metodo è stato contrassegnato con un SoapExtensionAttribute; riceve l’attributo nel secondo argomento.

Public Overloads Overrides Function GetInitializer( _ ByVal methodInfo As LogicalMethodInfo, _ ByVal attribute As SoapExtensionAttribute) As Object ‘ Ottiene un riferimento fortemente tipizzato all’attributo. Dim scaAttr As SoapCustomAuthenticationAttribute = _ DirectCast(attribute, SoapCustomAuthenticationAttribute) ‘ Restituisce la proprietà RequiredSubscriptionLevel. Return scaAttr.RequiredSubscriptionLevel End Function ‘ ...(gli altri metodi di questa classe come verrà descritto in ‘ seguito)... § End Class

A differenza del metodo GetInizializer – che viene invocato solo una volta durante il tempo di vita dell’estensione SOAP – il metodo Initialize viene invocato ogni volta che ASP.NET riceve una richiesta che deve essere passata all’estensione SOAP. Il metodo Initialize riceve un argomento Object uguale al valore di ritorno del metodo GetInitializer. In questo esempio, tale valore rappresenta il livello di sottoscrizione minimo necessario ad accedere al metodo dell’XML Web service che viene invocato. Tipicamente, il codice del metodo Initialize salva tale valore in una variabile privata in modo da renderla accessibile da altri metodi della classe: ‘ Il metodo Initialize viene invocato con il RequiredSubscriptionLevel ‘ intero nell’argomento. Dim RequiredSubscriptionLevel As Integer Public Overrides Sub Initialize(ByVal initializer As Object) ‘ Salva il livello di sottoscrizione richiesto per usi futuri. RequiredSubscriptionLevel = CInt(initializer) End Sub

Il terzo metodo ridefinito è ChainStream, il quale riceve lo stream utilizzato per inviare il testo XML dal client all’XML Web service e viceversa. Questo può essere lo stream originale utilizzato da ASP.NET per leggere e generare i dati, oppure uno stream creato da un’estensione SOAP con priorità inferiore. Il metodo ChainStream viene invocato prima che il messaggio SOAP passi attraverso ciascun stadio di elaborazione. Dal momento che lo scopo della maggior parte delle estensioni SOAP è quello di leggere e possibilmente modificare l’XML che viene trasmesso da e verso il client, è necessario creare un nuovo stream, salvare quest’ultimo e quello originale in una coppia di variabili locali e restituire lo stream appena creato: Dim oldStream As Stream Dim newStream As Stream


1166

Parte VI Applicazioni Internet

‘ ChainStream viene invocato all’atto dell’istanziazione della classe SoapExtension. Public Overrides Function ChainStream(ByVal stream As Stream) As Stream ‘ Salva il vecchio stream. oldStream = stream ‘ Crea un nuovo stream e lo restituisce. newStream = New MemoryStream() Return newStream End Function

L’ultimo metodo ridefinito che ASP.NET invoca all’interno dell’estensione SOAP è anche il più importante del gruppo in quanto implementa l’azione vera e propria. Il metodo ProcessMessage viene invocato una sola volta per ogni possibile stato del messaggio, ed è possibile specificare i diversi stati tramite la proprietà Stage dell’oggetto SoapMessage passato come argomento: ‘ Questo metodo viene invocato più volte per ogni messaggio. Public Overrides Sub ProcessMessage(ByVal message As SoapMessage) Select Case message.Stage Case SoapMessageStage.BeforeDeserialize ‘ Copia dal nuovo stream a quello nuovo. CopyStream(oldStream, newStream) newStream.Position = 0 Case SoapMessageStage.AfterDeserialize ‘ Il messaggio viene deserializzato, pertanto è possibile leggere ‘ le intestazioni del messaggio. If EvalSubscriptionLevel(message) < RequiredSubscriptionLevel Then ‘ Solleva un’eccezione se il livello di sottoscrizione non è ‘ adeguato. Throw New SoapException(“Insufficient subscription level”, _ SoapException.ClientFaultCode) End If Case SoapMessageStage.BeforeSerialize Case SoapMessageStage.AfterSerialize ‘ Copia dal nuovo stream a quello vecchio. newStream.Position = 0 CopyStream(newStream, oldStream) End Select End Sub ‘ Questa è la routine ausiliaria che sposta i dati da uno stream all’altro. Private Sub CopyStream(ByVal source As Stream, ByVal dest As Stream) Dim sr As New StreamReader(source) Dim sw As New StreamWriter(dest) sw.WriteLine(sr.ReadToEnd) sw.Flush() End Sub

EvalSubscriptionLevel è una funzione privata che analizza tutte le intestazioni dell’oggetto SoapMessage e cerca un oggetto AccountInfoHeader. Se viene trovata un’intestazione di questo tipo, la funzione può determinare se username e password del client sono validi e restituisce il livello di sottoscrizione del client: Private Function EvalSubscriptionLevel(ByVal message As SoapMessage) _ As Integer ‘ Verifica se esiste un’intestazione di tipo AccountInfoHeader. Dim header As SoapHeader For Each header In message.Headers If TypeOf header Is AccountInfoHeader Then ‘ Converte nel tipo appropriato. Dim accountInfo As AccountInfoHeader = _


Capitolo 26 Gli XML Web service

1167

DirectCast(header, AccountInfoHeader) ‘ Verifica le credenziali dell’utente e restituisce il livello ‘ di sottoscrizione. Return GetUserSubscriptionLevel(accountInfo.UserName, _ accountInfo.Password) End If Next ‘ Se si arriva a questo punto, mancano le credenziali oppure non sono ‘ valide. Return -1 End Function

(Ho omesso il codice sorgente della classe GetUserSubscriptionLevel in quanto è identico a quello illustrato in precedenza nel paragrafo “Autenticazione personalizzata” di questo capitolo). Quello che segue è il codice lato client che invoca un metodo protetto con un attributo SoapCustomAuthentication: ‘ Crea informazioni relative all’account nell’intestazione. Dim accountInfo As New localhost.AccountInfoHeader() accountInfo.UserName = “JoeDoe” accountInfo.Password = “jdpwd” ‘ Associa le informazioni sull’account all’oggetto proxy. Dim service As New localhost.SampleService() service.AccountInfoHeaderValue = accountInfo Try ‘ Questa chiamata ha successo solo se l’utente JoeDoe ha un livello ‘ di sottoscrizione maggiore o uguale a 2. Dim res As Boolean = service.YetAnotherProtectedMethod() Catch ex As Exception MsgBox(ex.Message, MsgBoxStyle.Critical) End Try

La documentazione del .NET Framework SDK contiene la descrizione e il codice di una classe di estensioni SOAP che salva tutto il testo XML in input e in output in un file. Oltre a fornire un buon esempio di come utilizzare le estensioni SOAP, rappresenta un eccellente strumento di diagnostica che può essere di grande aiuto quando le cose non vanno come dovrebbero. Malgrado gli scopi differenti, sarà possibile vedere come la struttura dell’esempio dell’SDK sia fondamentalmente la stessa dell’estensione SOAP che ho descritto in queste pagine.

Le estensioni SOAP lato client Fin qui, ho descritto le cosiddette estensioni SOAP lato server, che rappresentano di gran lunga il tipo di estensione SOAP più comune. Tuttavia, è anche possibile implementare estensioni SOAP lato client. Ad esempio, un’estensione SOAP lato client potrebbe rivelarsi necessaria per decriptare l’XML inviato da un XML Web service e criptato da un’estensione SOAP lato server. È possibile attivare un’estensione SOAP lato client tramite gli attributi personalizzati, esattamente come accade per quelle lato server, a eccezione del fatto che tali attributi vengono applicati ai metodi della classe proxy. Un’estensione SOAP lato client passa attraverso gli stessi quattro stati visti per quelle lato server, ma segue un ordine differente: BeforeSerialize e AfterSerialize quando il messaggio SOAP viene inviato all’XML Web service; BeforeDeserialize e AfterDeserialize quando l’XML risultante viene ricevuto dall’XML Web service (Figura 26-13.) So bene che a questo punto dovrei mostrare un esempio di estensione SOAP lato client, ma purtroppo devo fermarmi qui, altrimenti sarà troppo difficile rilegare questo volume.


provax