Introduzione a WCF 4.0 - Error Handling

Scritto da  Pietro Libro il mercoledì 27 ottobre 2010
Linguaggio: C#,VB   •  Framework: 4.0   •  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).

Figura_1

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:

Figura_2

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.

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:

Figura_4

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.

Figura_5

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.

Figura_6

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:

Figura_7

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

 
x