Introduzione a WCF 4.0 - Error Handling
Scritto da
Pietro Libro
il
mercoledì 27 ottobre 2010
Linguaggio:
•
Framework:
•
Livello: 100
Download sorgenti
Nel secondo articolo della serie abbiamo visto come realizzare
un semplice servizio WCF. Fin qui tutto ok, ma non abbiamo ancora
visto un argomento, a mio avviso fondamentale: la gestione degli
errori. Per quanto il nostro servizio sia stato progettato bene,
testato, revisionato e quant'altro, possiamo sempre imbatterci
in situazioni del tipo: a) non abbiamo scritto o eseguito
nessun test (male!), b) abbiamo scritto ed eseguito i test, ma
qualcosa è sfuggito c) siamo i migliori tester del mondo, ma non
abbiamo considerato eventuali problemi hardware (interruzioni delle
connessioni) o relativi a terzi (dovuti ad esempio dall'utilizzo di
librerie esterne). In questi casi, che si fa?
Una delle prime azioni che vogliamo intraprendere è
sicuramente notificare utente di quato accaduto (utilizzando
metodi user frendly e senza divulgare informazioni come
stringhe di connessione) utilizzando ad esempio una Windows Form o
una pagina ASP.NET. In seconda istanza vorremmo loggare l'errore
così da poter capire cosa sia successo. Vediamo come fare tutto
questo in WCF. Riprendiamo il servizio Calculator implementato la
scorsa volta e aggiungiamo al nostro ICalculator un nuovo
OperationContract che utilizzeremo per esporre un metodo della
nostra calcolatrice per eseguire la moltiplicazione di un vettore
pe uno scalare:
[OperationContract()]
int[] Scalar(int[] numbers, int coefficient);
Codice in Calculator.cs:
public double[] Scalar(double[] numbers, double coefficient)
{
for (int i = 0; i < numbers.Length; i++)
{
numbers[i] = numbers[i] * coefficient;
}
return numbers;
}
Per testare il funzionamento del nostro servizio aggiungiamo un
progetto console il quale rappresenta un possibile utilizzatore
(client) della nostra calcolatrice. Tasto destro sulla nostra
soluzione nel SolutionExplorer e poi Add\New
Project. Tra i vari template installati scegliamo Visual
C#\Console Application e denominiamo il nostro progetto in
Domus.WCF.UI.Console.CalcuatorService (al solito, il nome
non è fondamentale per la buona riuscita dell'applicazione).

A questo punto aggiungiamo un riferimento al nostro servizio,
niente di più facile: tasto destro sul nome del progetto appena
aggiunto alla solution, poi dal menu contestuale visualizzato
scegliamo Add Service Reference, come mostrato in
figura:

Nella finestra visualizzata click sul bottone con la dicitura
Discover e scegliamo il servizio WCF. Nella parte inferiore della
finestra, sotto Namespace, modifichiamo il contenuto del
campo di testo da ServiceReference1 in
CalculatorService. Il tutto dovrebbe essere simile a
quanto mostrato in figura 3.

Dopo aver premuto "OK" e qualche secondo di attesa,
Visual Studio avrà aggiunto al nostro progetto Console tutto
il necessario per interagire con la nostra calcolatrice.
Aggiungiamo un po' di codice nel Program.cs:
using (CalculatorService.CalculatorClient calculator = new CalculatorService.CalculatorClient())
{
int[] numbers = new int[] { 1, 2, 3, 4, 5 };
int coefficient = 5;
int[] newNumbers = calculator.Scalar(numbers, coefficient);
//Ok, write 5,10,15,20,25.
foreach (int n in newNumbers)
{
System.Console.WriteLine("New int={0}", n);
}
}
System.Console.ReadKey();
Assicuriamoci di aver impostato come progetto di avvio il
progetto Console e poi premiamo F5. Dovremmo ritrovarci con una
schermata simile a questa:

Fin qui tutto ok. Ma supponiamo che l'interazione con la nostra
calcolatrice non sia così semplice, e che per qualche motivo il
vettore di interi numbers sia impostato a null prima di
richiamare Scalar. Si verificherebbe il Patatrack come
mostrato da VS non appena premiamo F5.

Effettivamente, la dialog mostrata da VS contiene una
descrizione dell'errore non proprio significativa, anzi ci dice di
abilitare questo misterioso IncludeExceptionDetailsInFault
presente in <serviceDebug>. Proviamo: apriamo
l'AppConfig del servizio (non del progetto console!) ed impostiamo
a true il valore di questo attributo e proviamo a rieseguire la
nostra applicazione. Sicuramente l'errore mostrato in figura
è più "umano" da comprendere per noi sviluppatori del servizio e
può farci capire quale possa essere l'errore, ma sicuramente non è
l'ideale per i nostri clienti.

Prima di procedere con la scrittura di altro codice è necessario
un approfondimento sul concetto di eccezione in WCF. Fino ad ora
abbiamo visto che in caso di errore viene sollevata un'eccezione
che dovrebbe essere comunicata in un qualche modo a chi usufruisce
del servizio. Quello che bisogna tenere in mente quando si progetta
un servizio è che potrebbe essere utilizzato anche da applicazioni
che non fanno parte del mondo .NET (altrimenti che interoperabilità
SOA avremmo ?), ma le eccezioni di cui stiamo parlando sono
qualcosa legate al mondo .NET. Che si fa? WCF rispetta gli
standard previsti dalle specifiche WS-* le quali introducono il
concetto di SOAP Fault.
SOAP Fault element
In presenza di errori, un elemento SOAP Fault appare come figlio
dell'elemento Body del messaggio SOAP (questo elemento può apparire
una sola volta). I sotto elementi (figli dell'elemento SOAP
Fault) sono:
- faultcode: contiene un codice che identifica il
fault
- faultstring: una descrizione comprensibile del
fault
- faultactor: specifica chi ha causato il fault
verificato
- detail: contiene informazioni specifiche
sull'errore
In WCF, quello che succede quando si verifica un'eccezione è che
il server invia un SOAP Fault al client che interpreta il messaggio
e solleva un'eccezione nel codice che raggiunge automaticamente il
client sottoforma di CommunicationException. Questo tipo
di eccezione comporta la chiusura del canale con il proxy. Ogni
ulteriore tentativo di chiamare il servizio solleverà un'eccezione
lato client. Come possiamo utilizzare il SOAP Fault nelle nostre
applicazioni ? In .Net, ed in particolare WCF, la classe utilizzata
per rappresentare un Faut è la classe FaultException,
ancora meglio la sua controparte generica
FaultException<T> attraverso il quale è possibile
inviare informazioni dettagliate sull'errore al client,
specificando un "tipo" serializzabile per T.
Riprendiamo il progetto Domus.WCF.CalculatorService e
aggiungiamo una nuova classe denominata
CalculatorFaultContract, come di seguito specificata:
[DataContract]
public class CalculatorFaultContract
{
[DataMember]
public string ErrorMessage { get; set; }
}
Modifichiamo l'interfaccia del nostro servizio decorando il
metodo Scalar con l'attributo FaultContract,
ottenendo:
[OperationContract()]
[FaultContract(typeof(CalculatorFaultContract))]
int[] Scalar(int[] numbers, int coefficient);
Modifichiamo il codice del metodo Scalar in questo modo:
public int[] Scalar(int[] numbers, int coefficient)
{
if (numbers == null)
{
CalculatorFaultContract fc = new CalculatorFaultContract();
fc.ErrorMessage ="numbers array is null. Set properly this array before use this method.";;
throw new FaultException<CalculatorFaultContract>(fc,
new FaultReason("numbers is null."),
new FaultCode("Calculator"));
}
for (int i = 0; i < numbers.Length; i++)
{
numbers[i] = numbers[i] * coefficient;
}
return numbers;
}
Dove non facciamo altro che verificare se il vettore di interi
passato come argomento del metodo è null. Nel caso lo fosse,
solleviamo un FaultContract di tipo
CalculatorFaultContract che il nostro client dovrebbe
intercettare in modo opportuno. Prima di modificare il codice della
nostra console ricordiamoci di eseguire un aggiornamento del
riferimento al nostro servizio tramite il Solution
Explorer. Procediamo con la modifica:
using (CalculatorService.CalculatorClient calculator = new CalculatorService.CalculatorClient())
{
try
{
int[] numbers = new int[] { 1, 2, 3, 4, 5 };
int coefficient = 5;
int[] newNumbers = calculator.Scalar(numbers, coefficient);
//Ok, write 5,10,15,20,25.
foreach (int n in newNumbers)
{
System.Console.WriteLine("New int={0}", n);
}
//Again, but this time, numbers is null.
numbers = null;
newNumbers = calculator.Scalar(numbers, coefficient);
foreach (int n in newNumbers)
{
System.Console.WriteLine("New int={0}", n);
}
}
catch (TimeoutException te)
{
System.Console.WriteLine("Timeout expired: " + te.Message);
}
catch (FaultException<CalculatorService.CalculatorFaultContract> cf)
{
System.Console.WriteLine(cf.Detail.ErrorMessage);
}
catch (FaultException fe)
{
System.Console.WriteLine(fe.Message);
}
catch (CommunicationException ce)
{
System.Console.WriteLine(ce.Message);
}
catch (Exception e)
{
System.Console.WriteLine(e.Message);
}
}
System.Console.ReadKey();
Nel codice, oltre alla gestione della FaultException tipizzata è
stata introdotta la sequenza gerarchica di come dovrebbero essere
intercettate le eccezioni. Se ad esempio dopo TimeoutException
intercettassimo la CommunicationException, la nostra FaultException
non verrebbe mai intercettata (tra l'altro questo è impedito
direttamente da VS che segnala un errore). Eseguendo
l'applicazione, otterremo quanto mostrato dalla figura
seguente:

Per concludere questo articolo non ci resta che parlare della
gestione degli errori "lato server". A tal fine è necessario
introdurre l'interfaccia IErrorHandler (contenuta in
System.ServiceModel.Dispatcher). Questa interfaccia definisce due
metodi :ProvideFault e HandleError.
ProvideFault
Senza considerare il tipo dell'eccezione, questo metodo
è invocato immediatamente prima che l'eccezione sia inviata
al client. Questo metodo può essere utilizzato per modificare last
minute l'eccezione prima di essere inviata. Un possibile utilizzo
di questo metodo e rimuovere nel resto del codice del servizio i
vari try…catch e centralizzare la gestione degli errori in questo
metodo. Il codice eseguito dovrebbe essere eseguito il prima
possibile, dato che la chiamata del servizio è bloccata fintantoché
non sono terminate le operazioni qui presenti.
HandleError
In questo metodo può essere inserito tutto il codice per eseguire
(ad esempio) il logging degli errori senza però interrompere la
chiamata al servizio (il thread in cui viene eseguito questo
codice è diverso da quello in cui viene eseguito il servizio). Per
accedere all'eccezione sollevata dal servizio è sufficiente
utilizzare l'istanza della classe Exception passata come unico
argomento di HandleError. Come è possibile vedere dalla firma del
metodo, HandleError ritorna un valore booleano: possono esistere
più oggetti che eseguono la gestione delle eccezioni, se si
restituisce true, l'errore viene considerato gestito ed il
controllo non passa ai successivi, altrimenti, sul false, verrà
considerato il prossimo oggetto nella pipeline.
Vediamo praticamente cosa succede. Aggiungiamo al nostro servizio
una nuova classe denominata CalculatorErrorHandler come di
seguito specificata:
public class CalculatorErrorHandler : Attribute, IErrorHandler,IServiceBehavior
{
#region IErrorHandler Members
public bool HandleError(Exception error)
{
//Error logging.
string errorMessage = string.Format("Application:{0},method:{1},StackTrace:{2}",
error.Source, error.TargetSite.Name, error.StackTrace);
Debug.WriteLine(errorMessage);
return true;
}
public void ProvideFault(Exception error,
System.ServiceModel.Channels.MessageVersion version,
ref System.ServiceModel.Channels.Message fault)
{
if (error is NullReferenceException)
{
//Exception that we want to send back to the client.
CalculatorFaultContract fc = new CalculatorFaultContract();
fc.ErrorMessage = "numbers array is null. Set properly this array before use this method.";
FaultException<CalculatorFaultContract> exception =
new FaultException<CalculatorFaultContract>(
fc,
new FaultReason("CalculatorService Failure."),
new FaultCode("Calculator"));
// Creates a message fault.
MessageFault messageFault = exception.CreateMessageFault();
fault = Message.CreateMessage(version, messageFault, exception.Action);
}
}
#endregion
#region IServiceBehavior Members
public void AddBindingParameters(ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase,
System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints,
BindingParameterCollection bindingParameters)
{
}
public void ApplyDispatchBehavior(ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase)
{
foreach (var channelDispatcherBase in serviceHostBase.ChannelDispatchers)
{
var channelDispatcher = channelDispatcherBase as ChannelDispatcher;
channelDispatcher.ErrorHandlers.Add(new CalculatorErrorHandler());
}
}
public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
{
}
#endregion
}
Volendo utilizzare un approccio imperativo (senza utilizzare il
file di configurazione, con gli eventuali vantaggi/svantaggi) la
classe CalculatorErrorHandler eredita da Attribute ed
implementa le interfacce IErrorHandler (di cui abbiamo
parlato precedentemente) e IServiceBehavior. Quest'ultima
interfaccia è necessaria per inserire la classe
CalculatorErrorHandler nella pipeline di WCF, altrimenti
verrebbe completamente ignorata. A tal fine scriviamo il codice
presente in ApplyDispatchBehavior. Nell'implementazione
del metodo HandleError non facciamo altro che
eseguire il logging dell'errore scrivendo direttamente nella
finestra di output di VS, rispettivamente l'applicazione che ha
generato l'errore (in questo caso il nostro servizio, ma non è
detto se si utilizzano librerie di terzi), il metodo ed il
contenuto dello StackTrace. Invece della finestra di
output si potrebbe essere utilizzato un file di testo o un
database.
Nell'implementazione del metodo ProvideFault verifichiamo
che l'eccezione sollevata sia del tipo che ci aspettiamo
(NullReferenceException in questo caso) e creiamo come già
visto, un'istanza di
FaultException<CalculatorFaultContract>. Per
"spedire" il tutto verso il client che ha eseguito la chiamata,
impacchettiamo il tutto all'interno di un'istanza della classe
Message che restituiamo a WCF tramite il parametro Fault.
Siamo giunti così al termine di questa terza puntata dedicata a
WCF 4. Sicuramente restano molti dubbi su alcuni degli oggetti
visti in questo articolo soprattutto per chi si avvicina per la
prima a questa tecnologia. Ci riserviamo di sciogliere questi dubbi
in altri articoli riguardanti Windows Communication Foundation.
Nella quarta ed ultima parte di questa serie vedremo i diversi modi
di eseguire l'hosting dei nostri servizi.
In allegato il progetto di esempio dell'articolo.
Tags: WCF,SOA,Exception,Error Handling