Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

La gestione degli errori in un'app Windows Store

Link copiato negli appunti

In un'applicazione Windows Store, come del resto in una qualunque altra applicazione .NET, la gestione degli errori che possono verificarsi durante l'esecuzione rappresenta un momento particolarmente delicato.

Il CLR fornisce un modello uniforme per la notifica degli errori, in base al quale qualunque operazione è tenuta a notificare eventuali errori attraverso il sollevamento di eccezioni.

Gestire le eccezioni in .NET

Questo significa che tutti i pattern e le best practice che normalmente si applicano in .NET possono (e devono) essere usati anche per la gestione degli errori in un'app Windows Store.

I principi generali da seguire possono essere riassunti come segue.

try/catch/finally

In primo luogo, è importante usare un blocco try/catch ogni volta che c'è la possibilità che qualcosa possa non funzionare, soprattutto quando si tenta di accedere a una risorsa che potrebbe essere non disponibile o mancante, o quando si utilizzano tipi e funzioni sulle quali non si ha alcun controllo (come nel caso di librerie di terze parti, servizi remoti, ecc.). Se il codice accede a risorse unmanaged è bene aggiungere anche una clausola finally per eseguire le operazioni di "pulizia" necessarie.

È buona norma usare blocchi catch multipli quando vi è la possibilità che il codice sollevi diversi tipi di eccezione, in modo da prevedere una risposta specifica per ciascuna di esse. In questo caso, i vari catch devono essere ordinati in successione dal più specifico al più generale. Per esempio, nel codice che segue il primo catch intercetta solo le eccezioni di tipo UnauthorizedAccessException; se il tipo di eccezione non coincide con quello specificato, questa verrà intercettata dal secondo, e più generico, blocco catch.

try
{
    Geoposition pos = await this._geo.GetGeopositionAsync();
    LatitudeTextBlock.Text = "Latitude: " + pos.Coordinate.Latitude.ToString();
    LongitudeTextBlock.Text = "Longitude: " + pos.Coordinate.Longitude.ToString();
}
catch (UnauthorizedAccessException ex)
{
    // L'app ha bisogno del permesso per accedere alla tua posizione
}
catch (Exception ex)
{
    // Impossibile recuperare le informazioni sulla posizione
}

Eccezioni custom

In secondo luogo, quando creiamo le nostre classi custom per gestire le eccezioni (pratica comune nelle soluzioni multitier), è necessario derivarle dalla classe base Exception o da una delle classi da questa derivate, implementando tutti i costruttori della classe base, come mostrato nel prossimo snippet:

public class MyCustomException : Exception
{
    public MyCustomException()
    {
    }
    public MyCustomException(string message)
        : base(message)
    {
    }
    public MyCustomException(string message, Exception inner) :
        base(message, inner)
    {
    }
}

In terzo luogo, quando nel nostro codice vogliamo indicare che si è verificato un errore durante una qualche operazione, è sempre meglio sollevare un'eccezione, piuttosto che restituire codici di errore, valori booleani, messaggi testuali o valori null. I valori restituiti, infatti, non propagandosi attraverso la catena di chiamate, potrebbero andare persi e quindi lasciare l'app in uno stato inconsistente o determinare comportamenti imprevedibili (restituire null può invece essere un'opzione valida nel caso di "errori" particolari, come ad esempio un record non trovato).

Rimettere le cose in ordine

Quando viene sollevata un'eccezione, è importante ricordarsi di eliminare qualunque risultato intermedio: il chiamante deve poter contare sul fatto che, se qualcosa va storto durante l'esecuzione di un metodo, lo stato dell'oggetto coinvolto non è stato alterato.

Infine, quando si ha a che fare con eccezioni a livello di user interface, è importante includere una descrizione localizzata per ciascun messaggio da mostrare all'utente.

Come abbiamo accennato, le best practice qui discusse si applicano a qualunque applicazione .NET. Tuttavia, quando abbiamo a che fare con un'applicazione Windows Store, ci sono alcune cose da considerare. Per prima cosa, infatti, il framework XAML include la possibilità di intercettare eccezioni che non sono state gestite dal codice applicativo.

In secondo luogo, le eccezioni sollevate da un device (sensori, webcam, ecc.) richiedono di essere gestite in modo specifico (in genere queste eccezioni sono sollevate perché mancano i permessi per accedere alle risorse, o perché il device non è presente sul sistema). In terzo luogo, da momento che un gran numero di API di WinRT sono basate sul pattern async/await, è importante imparare a gestire le eccezioni che possono essere sollevate durante lo svolgimento di operazioni asincrone.

Intercettare eccezioni a livello applicativo

La classe Application, che incapsula l'applicazione Windows Store espone i relativi servizi, espone uno speciale evento denominato UnhandledException, il quale viene sollevato ogniqualvolta il framework XAML incontra un'eccezione che non è stata gestita dal codice applicativo.

A questo proposito, la documentazione ufficiale MSDN sottolinea come questo evento viene sollevato solo quando non c'è più alcuna possibilità che il codice applicativo intercetti l'eccezione. Questo significa che un'eccezione sollevata durante l'esecuzione di una funzione, e non intercettata tramite catch, si propagherà tramite la catena di chiamate, dando a ciascun chiamante nella catena la possibilità di gestire l'eccezione non precedentemente intercettata. Solo se anche l'ultimo chiamante nella catena avrà mancato di intercettare l'eccezione, e quindi venuta meno ogni possibilità di gestirla, il framework XAML solleverà l'evento UnhandledException per segnalare un'eccezione non gestita (unhandled).

Prima di vedere come gestire questo evento da codice, è interessante osservare cosa accade "under the hood" quando viene sollevata un'eccezione non gestita. Il listato che segue mostra il codice auto-generato che rappresenta l'entry point dell'applicazione (per ispezionare il codice è sufficiente cercare il file object/App.g.i.cs).

#pragma checksum "D:\Projects\Html.it\Demo.Html.it.AdvancedWin8\Demo.Html.it.MediaCaptureSample.CS\App.xaml" "{406ea660-64cf-4c82-b6f0-42d48172a799}" "EB43B2CF0DF763A03871FA380C4D70B4"
//------------------------------------------------------------------------------
// 
//     This code was generated by a tool.
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// 
//------------------------------------------------------------------------------
namespace Demo.Html.it.MediaCaptureSample.CS
{
#if !DISABLE_XAML_GENERATED_MAIN
    public static class Program
    {
        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Windows.UI.Xaml.Build.Tasks"," 4.0.0.0")]
        [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
        static void Main(string[] args)
        {
            global::Windows.UI.Xaml.Application.Start((p) => new App());
        }
    }
#endif
    partial class App : global::Windows.UI.Xaml.Application
    {
        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Windows.UI.Xaml.Build.Tasks"," 4.0.0.0")]
        private bool _contentLoaded;
        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Windows.UI.Xaml.Build.Tasks"," 4.0.0.0")]
        [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
        public void InitializeComponent()
        {
            if (_contentLoaded)
                return;
            _contentLoaded = true;
#if DEBUG && !DISABLE_XAML_GENERATED_BINDING_DEBUG_OUTPUT
            DebugSettings.BindingFailed += (sender, args) =>
            {
                global::System.Diagnostics.Debug.WriteLine(args.Message);
            };
#endif
#if DEBUG && !DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION
            UnhandledException += (sender, e) =>
            {
                if (global::System.Diagnostics.Debugger.IsAttached) global::System.Diagnostics.Debugger.Break();
            };
#endif
        }
    }
}

Per prima cosa, la classe Program crea un'istanza della classe App (che incapsula il codice applicativo dell'applicazione) e la passa al metodo Windows.UI.Xaml.Application.Start. All'interno della classe App, il codice auto-generato sottoscrive due eventi: l'evento BindingFailed, il quale viene sollevato quando un binding nel codice XAML non può essere risolto, e l'evento UnhandledException.

Entrambi gli handler di questi eventi sono circondati da statement con compilazione condizionale, per cui le relative istruzioni vengono compilate solo se è stata scelta la modalità DEBUG. In particolare, quando un'app incontra un'eccezione non gestita durante il debug, il codice auto-generato imposta il debugger in modalità "break". In modalità RELEASE, invece, non vi è alcun debugger da interrompere, per cui il codice condizionale non verrà compilato e l'evento UnhandledException non verrà gestito, con conseguente chiusura dell'applicazione.

È importante sottoscrivere questo evento nel costruttore della classe App, così da poter intercettare qualunque eccezione non gestita sollevata dal codice applicativo. Il prossimo snippet mostra un esempio:

public App()
{
    this.InitializeComponent();
    this.Suspending += OnSuspending;
    this.UnhandledException += App_UnhandledException;
}
private void App_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
    // gestisci qui l'eccezione.
    // se vuoi evitare di terminare l'app, imposta la proprietà Handled a true
    e.Handled = true;
}

Se nell'event handler impostiamo a true la proprietà Handled dell'oggetto UnhandledExceptionEventArgs ricevuto come parametro, stiamo segnalando al framework di non processare ulteriormente l'eccezione (con conseguente chiusura dell'applicazione) perché non necessario. In questo caso, l'applicazione continuerà ad essere eseguita.

Tuttavia, è importante ricordare come Microsoft scoraggi fortemente questa prassi per almeno due buone ragioni:

  1. gli argomenti ricevuti dall'event handler non offrono sufficienti informazioni per decidere se tenere l'applicazione in esecuzione sia sicuro o meno. Dal momento che non c'è modo di sapere da dove arriva l'eccezione, parti dell'applicazione potrebbero trovarsi in uno stato inconsistente a seguito dell'errore. Inoltre, ogni informazione relativa al tipo di eccezione, al messaggio d'errore e allo stack trace dell'eccezione (che possono essere recuperate tramite la proprietà Exception della classe UnhandledExceptionEventArgs) non coincidono necessariamente con quelli propri dell'eccezione originaria, per cui non vi può essere certezza circa il tipo di eccezione a cui ci troviamo di fronte.
  2. se un'eccezione viene sollevata durante l'esecuzione di determinate operazioni, il framework considererà queste operazioni come non recuperabili, perché sa che lasceranno il sistema in uno stato inconsistente. In questi scenari, l'app verrà comunque terminata, a prescindere dal fatto di aver impostato la proprietà Handled a true.

Per queste ragioni, è sempre meglio non usare questa proprietà per evitare la chiusura dell'applicazione. Piuttosto, lo scopo dell'evento UnhandledException è quello di consentire, in presenza di un'eccezione non gestita (e dunque non prevista), di "prepararsi" alla chiusura dell'applicazione, ad esempio per loggare l'eccezione (dal momento che è sfuggita a qualunque blocco try/catch, l'handler dell'evento è l'ultimo posto in cui possiamo sperare di loggare l'eccezione), o magari salvare dati temporanei nello storage.

Dobbiamo anche aggiungere che questo evento incontra anche altre limitazioni: secondo la documentazione ufficiale MSDN, infatti, eccezioni che non sono connesse con il framework XAML (come ad esempio eccezioni sollevate da worker thread) non determinano la sollevazione dell'evento UnhandledException.

Gestire gli errori dei device

Alcune delle funzionalità (capability) dichiarate nell'application manifest, come il microfono, la camera e il location provider, sono considerati "device sensibili" ("sensitive device") poiché possono rivelare informazioni personali (come la posizione geografica) che l'utente potrebbe mantenere riservate. Per questa ragione, la prima volta che un'app cerca di accedere a uno di questi device, dichiarato nell'application manifest, il sistema chiede all'utente il permesso di utilizzare il device prima che l'app possa iniziare a usarlo. La prossima immagine mostra il dialog box con cui viene richiesto il permesso, da parte dell'app, di accedere alla webcam.

Una volta che l'utente ha prestato il suo consenso, questo può essere revocato in qualunque momento tramite le impostazioni Privacy nel pannello Permissions, attivabile mediante il charm Settings, come illustrato nella prossima immagine.

Per assicurare agli utenti una buona user experience, è importante che l'applicazione che ha bisogno di accedere a device sensibili sia in grado di gestire gli errori che possono insorgere quando si tenta di accedere a una funzionalità che non è disponibile. Le ragioni per cui l'accesso alla risorsa può fallire sono le seguenti:

  • l'utente non ha prestato il suo consenso all'uso del device nel dialog box;
  • l'utente ha revocato il permesso tramite il charm Settings;
  • Il dispositivo non è presente sul sistema.

Tuttavia, non tutte le API di WinRT presentano lo stesso comportamento quando si tenta di accedere a una capability per la quale l'app non ha i permessi necessari.

Per esempio, se l'app sfrutta la classe Windows.Media.Capture.MediaCapture per la cattura di audio, foto e video e la webcam non è presente sul sistema, una chiamata al metodo InitializeAsync dell'oggetto MediaCapture solleverà un'eccezione di tipo System.Exception. Se invece il dispositivo è presente, ma l'utente non ha dato il permesso di accedervi (o il permesso è stato revocato), il sistema solleverà un'eccezione di tipo UnauthorizedAccessException. In entrambi i casi, sarà necessario gestire gli errori in modo appropriato, informando l'utente che la funzionalità richiesta non è disponibile sul sistema o che questa non può essere utilizzata fino a quando l'utente non avrà espresso il suo consenso tramite il charm Settings. Il prossimo snippet mostra come gestire un semplice scenario di questo tipo:

private MediaCapture _mediaCaptureManager;
private async void StartMediaCapture_Click(object sender, RoutedEventArgs e)
{
    try
    {
        DeviceInformationCollection devInfoCollection = await DeviceInformation.FindAllAsync(DeviceClass.VideoCapture);
        MediaCaptureInitializationSettings settings = new             Windows.Media.Capture.MediaCaptureInitializationSettings();
        if (devInfoCollection.Count > 0)
        {
            settings.VideoDeviceId = devInfoCollection[0].Id;
            this._mediaCaptureManager = new MediaCapture();
            await this._mediaCaptureManager.InitializeAsync();
            capturePreview.Source = this._mediaCaptureManager;
            await this._mediaCaptureManager.StartPreviewAsync();
        }
        else
            ErrorMessageText.Text = "Nessun device connesso";
    }
    catch (UnauthorizedAccessException ex)
    {
        ErrorMessageText.Text = "L'app ha bisogno del permesso per accedere alla webcam";
    }
    catch (Exception ex)
    {
        ErrorMessageText.Text = "Impossibile inizializzare l'app";
    }
}

Prima di tentare di accedere alla webcam, il codice verifica che sul sistema sia presente almeno un device per la cattura video. Quindi, se il dispositivo viene trovato, il codice tenta di inizializzarlo tramite una chiamata al metodo InitializeAsync.

Se invece delle MediaCapture API, l'app sfrutta la classe CaptureCameraUI, il pattern seguito è differente. Ad esempio, se manca il permesso di accedere alla webcam, la chiamata al metodo CaptureFileAsync non solleva alcuna eccezione. Al contrario, la user interface mostrerà unicamente un messaggio che informa l'utente che l'app necessita del permesso di accedere alla webcam, come mostrato nella prossima immagine.

Se invece la webcam non è presente sul sistema, il messaggio mostrato all'utente sarà diverso:

Il prossimo listato mostra un esempio di utilizzo della classe CameraCaptureUI:

private async void TakePhoto_Click(object sender, RoutedEventArgs e)
{
    try
    {
        var camera = new CameraCaptureUI();
        var img = await camera.CaptureFileAsync(CameraCaptureUIMode.Photo);
        if (img != null)
        {
            var stream = await img.OpenAsync(FileAccessMode.Read);
            var bitmap = new BitmapImage();
            bitmap.SetSource(stream);
            ImageView.Source = bitmap;
        }
    }
    catch (Exception ex)
    {
        ErrorMessageText.Text = "Impossibile accedere alla webcam.";
    }
}

Per quanto il metodo CameraCaptureUI.CaptureFileAsync non sollevi un'eccezione quando manca il permesso di utilizzare la webcam, è comunque buona prassi circondare la chiamata alla API con un blocco try/catch. Questo perché, permessi a parte, c'è sempre qualcosa che può andare storto. Ad esempio, la prossima immagine mostra una System.Exception sollevata a causa di un imprevedibile malfunzionamento del device.

Quando si ha a che fare con un device per accedere al quale è richiede il permesso da parte dell'utente, la prima chiamata al device dovrebbe avvenire all'interno del thread di UI, in modo che il dialog box possa essere mostrato all'utente. Infine, nel caso di app per le quali l'uso di dispositivi sensibili non rappresenta la funzione principale, le line guida Microsoft suggeriscono di non mostrare messaggi di errore relativi a un device prima che l'utente abbia tentato di accedere alla relativa funzionalità.

Gestire le eccezioni asincrone

Come abbiamo visto in precedenza, le eccezioni sollevate all'interno di codice sincrono si propagano risalendo lungo la catena di chiamate, fino a quando non vengono intercettate in un blocco try/catch oppure finiscono "unhandled" e l'app viene terminata.

Nel codice asincrono, invece, le cose sono un po' più complicate a causa della presenza di più thread.

Fortunatamente, il pattern asincrono introdotto con C# 5.0 semplifica di molto la gestione delle eccezioni sollevate all'interno di codice asincrono. Infatti, grazie all'uso del pattern async/await, è adesso possibile collocare i propri blocchi try/catch attorno al codice asincrono proprio come si farebbe se ci trovassimo di fronte a normale codice sincrono, e l'eccezione sollevata durante l'esecuzione dell'operazione asincrona verrà intercettata dal blocco catch:

public async Task Foo()
{
    try
    {
        // chiamata al metodo asincrono che solleva l'eccezione
        await this.DoSomethingAsync();
    }
    catch (Exception ex)
    {
        // le eccezioni sollevate durante l'esecuzione di un'operazione asincrona
        // sono intercettate qui
    }
}
private Task DoSomethingAsync()
{
    // operazione asincrona
}

Quando viene sollevata un'eccezione durante l'esecuzione del metodo asincrono (DoSomethingAsync, nell'esempio), questa viene salvata nell'oggetto Task restituito dal metodo stesso.

È importante sottolineare che le eccezioni sono sollevate in corrispondenza della parola chiave await; questo significa che, se la chiamata al metodo asincrono non è immediatamente preceduta dall'istruzione await, l'eccezione non verrà sollevata sino a quando il task non verrà eseguito. Considera il seguente esempio:

private async Task DoSomethingAsync()
{
    try
    {
        var file = await this.ReadFileAsync();
        Task processResult = this.ProcessFileAsync(file);
        this.DisplayInfo();
        String result = await processResult;
    }
    catch (Exception ex)
    {
        // gestione dell'eccezione
    }
}

In questo codice, il metodo DisplayInfo verrà sempre eseguito, a prescindere dal fatto che si verifichi un errore durante l'esecuzione del metodo asincrono ProcessFileAsync. L'eventuale eccezione verrà sollevata solo quando il task verrà eseguito, ossia in corrispondenza dell'istruzione await. Il rischio, in questo caso, è che l'eccezione sollevata durante l'operazione asincrona finisca per essere "nascosta" da altre eccezioni sollevate durante l'esecuzione dei metodi che precedono l'istruzione await.

Se invece decidessimo di non usare del tutto l'istruzione await per "attendere" l'esecuzione di un metodo asincrono (ad esempio perché non siamo interessati al valore restituito dal metodo), l'eccezione eventualmente sollevata durante l'esecuzione dell'operazione asincrona verrà salvata in un task e conseguentemente ignorata (questo comportamento è proprio del framework .NET 4.5; nella versione 4.0 di .NET determinavano il crash dell'applicazione). Questo tipo di eccezione è definita "unobserved exception" e può portare l'applicazione in uno stato inconsistente, poiché questa assumerà che l'operazione (ad esempio l'invio di una mail o il salvataggio di dati su un servizio remoto) sia andata a buon fine.

La Task Parallel Library (TPL) espone un evento UnobservedTaskException che rappresenta una sorta di "ultima spiaggia" per intercettare questo tipo di eccezioni, in un modo che richiama da vicino quanto visto a proposito dell'evento UnhandledException visto in precedenza. Il prossimo snippet ne mostra un esempio:

public App()
{
    (...)
    TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
}
private void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
{
    foreach (Exception ex in e.Exception.Flatten().InnerExceptions)
    {
        // Logga l'eccezione
    }
    e.SetObserved();
}

L'oggetto di tipo UnobservedTaskExceptionEventArgs ricevuto dall'event handler espone una proprietà Exception che, a dispetto del suo nome, è di tipo AggregateException. Il metodo SetObserved può essere utilizzato per marcare un'eccezione come gestita (questo passaggio è opzionale in .NET 4.5, visto che comunque le unobserved exception di default non causano più la terminazione dell'app).

Ancora diverso è il caso in cui ad essere invocato è un metodo asincrono che, anziché restituire un oggetto Task, non ritorna alcun valore (void). Dal momento che questo tipo di metodo non può essere preceduto dalla clausola await, il chiamante non ha modo di intercettare le eccezioni sollevate durante l'esecuzione del metodo asincrono.

In questo caso, però, l'eccezione non verrà salvata in un task ma finirà "unhandled" e l'applicazione verrà terminata (non a caso, questo tipo di metodi sono gestiti dietro le quinte tramite la classe AsyncVoidMethodBuilder, mentre quelli che restituiscono un Task tramite la classe AsyncTaskMethodBuilder). È importante comprendere che i metodi asincroni marcati come void esistono solamente per essere utilizzati come handler di eventi di UI, come il click su un pulsante. In tutti gli altri casi, un metodo asincrono dovrebbe sempre restituire un Task, in modo da evitare effetti collaterali indesiderati.

Pattern asincroni ed eccezioni multiple

Finora abbiamo considerato il caso in cui un metodo asincrono solleva una singola eccezione. Tuttavia, quando si lavora con task e thread, non è raro imbattersi in eccezioni multiple concorrenti (ad esempio quando si utilizza il metodo Task.WhenAll per attendere il completamento di più task in parallelo, oppure quando si ricorre a parallel loop).

Per questi scenari, il framework .NET include un particolare tipo di eccezione denominata System.AggregateException, la quale permette di gestire eccezioni multiple.

Così come la classe Exception è in grado di incapsulare una singola istanza di tipo Exception ed esporla tramite la proprietà InnerException, la classe AggregateException permette di incapsulare una collezione di eccezioni. Questa collezione è esposta tramite la proprietà in sola lettura InnerExceptions (notare l'uso del plurale in questo caso).

Vale la pena di osservare come la classe AggregateException, derivando dalla classe base Exception, esponga anche una proprietà InnerException (al singolare). In questo caso, però, trattandosi di eccezioni multiple, questa proprietà conterrà la prima eccezione della collezione.

Il prossimo snippet mostra un esempio di utilizzo della classe AggregateException per intercettare eccezioni multiple concorrenti.

private void DoSomething()
{
    Task task1 = new Task(() =>
    {
        // codice omesso
    });
    Task task2 = new Task(() =>
    {
        // codice omesso
    });
    Task task3 = new Task(() =>
    {
        // codice omesso
    });
    task1.Start();
    task2.Start();
    task3.Start();
    try
    {
        Task.WaitAll(task1, task2, task3);
    }
    catch (AggregateException aex)
    {
        foreach (var inner in aex.InnerExceptions)
        {
            Debug.WriteLine("Tipo di eccezione: {0}", inner.GetType());
            Debug.WriteLine("Messaggio: {0}", inner.Message);
        }
    }
}

In alcuni scenari, un'AggregateException può contenere al suo interno altri oggetti di tipo AggregateException. In questi casi, può essere utile semplificare la gerarchia di eccezioni tramite il metodo AggregateException.Flatten. Questo metodo restituisce una nuova istanza di tipo AggregateException contenente una lista "flat" contenente le varie eccezioni. Il prossimo snippet ne mostra un esempio:

catch (AggregateException aex)
{
    foreach (Exception ex in aex.Flatten().InnerExceptions)
        this._log.SaveException(ex);
}

Quando si usa la classe AggregateException, può essere utile distinguere tra eccezioni previste (e conseguentemente da gestire) ed eccezioni invece impreviste, per le quali possiamo decidere di lasciar propagare lungo la catena di chiamate.

La classe AggregateException mette infatti a disposizione un metodo Handle che permette di specificare un predicate da invocare per ciascuna eccezione nella collezione. Questo predicate deve restituire true, se vogliamo evitare di propagare l'eccezione, altrimenti false (nel qual caso l'eccezione non gestita verrà inserita in una nuova eccezione AggregateException). Il codice che segue mostra un possibile utilizzo del metodo Handle, con un predicate che restituisce true ogni volta che incontra un certo tipo di eccezioni (ArgumentNullException e InvalidOperationException, nel nostro caso) e false per tutte le altre.

catch (AggregateException aex)
{
    aex.Handle((ex) =>
    {
        if (ex is ArgumentNullException)
        {
            //Logga l'eccezione
            return true;
        }
        if (ex is InvalidOperationException)
        {
            //Logga l'eccezione
            return true;
        }
        return false; // L'app viene terminata
    });
}

Ti consigliamo anche