C#:gestori di eventi di eccezione globali

C#:gestori di eventi di eccezione globali

Sono disponibili due eventi di eccezione globali in tutte le applicazioni .NET:

  • FirstChanceException:quando viene generata un'eccezione, questo evento viene generato prima di qualsiasi altra cosa.
  • UnhandledException:quando si verifica un'eccezione non gestita, questo evento viene attivato subito prima che il processo venga terminato.

Connetti questi gestori di eventi in Main() (prima che qualsiasi altra cosa sia stata eseguita), in questo modo:

using System.Runtime.ExceptionServices;

static void Main(string[] args)
{
	AppDomain.CurrentDomain.FirstChanceException += FirstChanceExceptionEventHandler;
	AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionEventHandler;

	throw new Exception("Example of unhandled exception");
}
private static void UnhandledExceptionEventHandler(object sender, UnhandledExceptionEventArgs e)
{
	Console.WriteLine($"UnhandledExceptionEventHandler - Exception={e.ExceptionObject}");
}
private static void FirstChanceExceptionEventHandler(object sender, FirstChanceExceptionEventArgs e)
{
	Console.WriteLine($"FirstChanceExceptionEventHandler - Exception={e.Exception}");
}
Code language: C# (cs)

Questo restituisce quanto segue prima dell'arresto anomalo:

FirstChanceExceptionEventHandler - Exception=System.Exception: Example of unhandled exception
   at ExampleConsoleApp.Program.Main(String[] args) in Program.cs:line 17

UnhandledExceptionEventHandler - Exception=System.Exception: Example of unhandled exception
   at ExampleConsoleApp.Program.Main(String[] args) in Program.cs:line 17Code language: plaintext (plaintext)

Si noti che l'evento FirstChanceException è stato attivato per primo. Questo evento viene attivato prima di tutto, anche catch blocchi (ne mostrerò un esempio di seguito). Puoi usarlo per la registrazione centralizzata delle eccezioni, invece di aver bisogno di try/catch blocchi solo per la registrazione di eccezioni sparse nel codice.

In questo articolo, analizzerò maggiori dettagli su questi gestori di eventi di eccezione globali, quindi mostrerò come vengono utilizzati in modo diverso nelle app WinForms e ASP.NET Core.

L'evento FirstChanceException con eccezioni gestite

Quando si verifica un'eccezione, viene prima indirizzata all'evento FirstChanceException. Quindi viene indirizzato al catch block appropriato.

Ecco un esempio:

AppDomain.CurrentDomain.FirstChanceException += (s, e) 
	=> Console.WriteLine($"FirstChanceExceptionEventHandler - Exception={e.Exception}");

try
{
	throw new Exception("Example of handled exception");
}
catch (Exception ex)
{
	Console.WriteLine($"In catch block. Exception={ex}");
}
Code language: C# (cs)

Questo produce quanto segue:

FirstChanceExceptionEventHandler - Exception=System.Exception: Example of handled exception
   at ExampleConsoleApp.Program.Main(String[] args) in Program.cs:line 19

In catch block. Exception=System.Exception: Example of handled exception
   at ExampleConsoleApp.Program.Main(String[] args) in Program.cs:line 19Code language: plaintext (plaintext)

Questo mostra che l'evento FirstChanceException viene sempre attivato per primo.

Eccezioni di stato corrotto

Le eccezioni di stato danneggiate (come le violazioni di accesso nel codice non gestito) provocano l'arresto anomalo del programma e i gestori di eventi di eccezione globale non vengono attivati. Il comportamento è diverso tra .NET Core e .NET Framework. Mostrerò esempi di entrambi di seguito.

Innanzitutto, ecco il codice che genera un'eccezione di violazione di accesso:

using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices;

static void Main(string[] args)
{
	AppDomain.CurrentDomain.FirstChanceException += FirstChanceExceptionEventHandler;
	AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionEventHandler;

	Marshal.StructureToPtr(1, new IntPtr(1), true);
}
private static void UnhandledExceptionEventHandler(object sender, UnhandledExceptionEventArgs e)
{
	Console.WriteLine($"UnhandledExceptionEventHandler - Exception={e.ExceptionObject}");
}
private static void FirstChanceExceptionEventHandler(object sender, FirstChanceExceptionEventArgs e)
{
	Console.WriteLine($"FirstChanceExceptionEventHandler - Exception={e.Exception}");
}
Code language: C# (cs)

.NET Core

L'esecuzione in un'app .NET Core comporta la seguente eccezione (scritta dal framework):

Fatal error. Internal CLR error. (0x80131506)
   at System.Runtime.InteropServices.Marshal.StructureToPtr(System.Object, IntPtr, Boolean)Code language: plaintext (plaintext)

Non instrada l'eccezione ai gestori di eventi di eccezione.

.NET Framework

Il comportamento predefinito in un'app .NET Framework è simile al comportamento di .NET Core. Si arresta in modo anomalo con la seguente eccezione:

Unhandled Exception: System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.
   at System.Runtime.InteropServices.Marshal.StructureToPtr(Object structure, IntPtr ptr, Boolean fDeleteOld)Code language: plaintext (plaintext)

Non ha instradato l'eccezione ai gestori di eventi di eccezione. Tuttavia, questo comportamento può essere modificato aggiungendo l'attributo HandleProcessCorruptedStateExceptions ai metodi:

[HandleProcessCorruptedStateExceptions]
private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
	Console.WriteLine($"UnhandledExceptionHandler - Exception={e.ExceptionObject}");
}
[HandleProcessCorruptedStateExceptions]
private static void CurrentDomain_FirstChanceException(object sender, FirstChanceExceptionEventArgs e)
{
	Console.WriteLine($"FirstChanceExceptionHandler - Exception={e.Exception}");
}
Code language: C# (cs)

Ora instrada l'eccezione ai gestori di eventi prima di arrestarsi in modo anomalo. Produce quanto segue:

FirstChanceExceptionHandler - Exception=System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.
   at System.Runtime.InteropServices.Marshal.StructureToPtr(Object structure, IntPtr ptr, Boolean fDeleteOld)
   at System.Runtime.InteropServices.Marshal.StructureToPtr[T](T structure, IntPtr ptr, Boolean fDeleteOld)

UnhandledExceptionHandler - Exception=System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.
   at System.Runtime.InteropServices.Marshal.StructureToPtr(Object structure, IntPtr ptr, Boolean fDeleteOld)
   at System.Runtime.InteropServices.Marshal.StructureToPtr[T](T structure, IntPtr ptr, Boolean fDeleteOld)
   at ExampleConsoleApp.Program.Main(String[] args) in Program.cs:line 15Code language: plaintext (plaintext)

Note:

  • Questa funzionalità è stata rimossa in .NET Core. Anche se utilizzi l'attributo HandleProcessCorruptedStateExceptions, verrà ignorato.
  • Puoi utilizzare l'attributo legacyCorruptedStateExceptionsPolicy app.config se non vuoi modificare il codice.
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
    </startup>
	<runtime>
		<legacyCorruptedStateExceptionsPolicy enabled="true" />
	</runtime>
</configuration>
Code language: HTML, XML (xml)

WinForms

WinForms ha un terzo evento di eccezione globale. Si chiama ThreadException. Questo può essere cablato in Main(), proprio come FirstChanceException e UnhandledException:

using System.Runtime.ExceptionServices;

[STAThread]
static void Main()
{
	Application.ThreadException += ThreadExceptionEventHandler;
	AppDomain.CurrentDomain.FirstChanceException += FirstChanceExceptionEventHandler;
	AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionEventHandler;
	
	Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
	Application.EnableVisualStyles();
	Application.SetCompatibleTextRenderingDefault(false);
	Application.Run(new frmMain());

}

private static void ThreadExceptionEventHandler(object sender, System.Threading.ThreadExceptionEventArgs e)
{
	MessageBox.Show($"ThreadExceptionEventHandler - Exception={e.Exception}");
}
private static void UnhandledExceptionEventHandler(object sender, UnhandledExceptionEventArgs e)
{
	MessageBox.Show($"UnhandledExceptionEventHandler - Exception={e.ExceptionObject}");
}

private static void FirstChanceExceptionEventHandler(object sender, FirstChanceExceptionEventArgs e)
{
	MessageBox.Show($"FirstChanceExceptionEventHandler - Exception={e.Exception}");
}
Code language: C# (cs)

L'evento ThreadException viene generato quando si verifica un'eccezione non gestita in un thread WinForms (ad esempio in un gestore di eventi di clic). Se un'eccezione non gestita si verifica altrove, genera invece l'evento UnhandledException. Mostrerò degli esempi di seguito.

Eccezione non gestita in un thread WinForms

I gestori di eventi di controllo (come i clic sui pulsanti) sono gestiti nei thread di WinForms. Quindi ecco un esempio di un'eccezione non gestita in un thread WinForms:

private void btnThrow_Click(object sender, EventArgs e)
{
	throw new Exception("btnThrow_Click exception");
}
Code language: C# (cs)

Ecco cosa succede. Innanzitutto, viene attivato l'evento FirstChanceException:

FirstChanceExceptionEventHandler - Exception=System.Exception: btnThrow_Click exception...Code language: plaintext (plaintext)

Quindi viene attivato l'evento ThreadException:

ThreadExceptionEventHandler - Exception=System.Exception: btnThrow_Click exception...Code language: plaintext (plaintext)

Quando non si utilizza l'evento ThreadException e si verifica un'eccezione non gestita in un thread WinForms, il comportamento predefinito è che mostra la finestra di dialogo di errore standard che afferma "Si è verificata un'eccezione non gestita...", che a volte è indesiderabile. Ecco perché è una buona idea usare l'evento ThreadException.

Eccezione non gestita altrove

L'evento ThreadException viene generato solo se l'eccezione si è verificata in un thread WinForms. Se un'eccezione non gestita si verifica altrove, viene attivato l'evento UnhandledException.

Ecco due esempi di eccezioni non gestite in thread non WinForms:

public frmMain()
{
	InitializeComponent();
	throw new Exception("Exception in form constructor");
}

private void btnThrow_Click(object sender, EventArgs e)
{
	var thread = new System.Threading.Thread(() =>
	{
		throw new Exception("Exception in a non-WinForms thread");
	});
	thread.Start();
}
Code language: C# (cs)

In entrambi questi esempi, viene attivato per primo l'evento FirstChanceException, seguito dall'evento UnhandledException. Quindi l'app si arresta in modo anomalo.

L'evento UnhandledException può essere davvero utile per la risoluzione dei problemi di eccezioni irreversibili in WinForms. Senza questo, quando si verifica un'eccezione irreversibile non gestita, l'app si arresta in modo anomalo senza alcuna indicazione di un problema. Se si verifica un'eccezione non gestita prima che il modulo venga disegnato, può essere ancora più difficile risolvere i problemi, perché non vedi nulla.

ASP.NET Core

Non suggerirei di usare l'evento FirstCanceException in un'app ASP.NET Core. Quando i controller generano eccezioni, questo evento viene attivato ripetutamente.

Puoi utilizzare l'evento UnhandledException per registrare le eccezioni di avvio, in questo modo:

using NLog;

private static Logger logger = LogManager.GetCurrentClassLogger();
public static void Main(string[] args)
{
	AppDomain.CurrentDomain.UnhandledException += (s, e) =>
	{
		logger.Error($"UnhandledExceptionHandler - Exception={e.ExceptionObject}");
		LogManager.Flush();
	};

	Host.CreateDefaultBuilder(args)
		.ConfigureWebHostDefaults(webBuilder =>
		{
		   webBuilder.UseStartup<Startup>();
		}).Build().Run();
}
Code language: C# (cs)

Diciamo che c'è un'eccezione non gestita in Startup.ConfigureServices():

public class Startup
{
	//rest of class
	public void ConfigureServices(IServiceCollection services)
	{
		services.AddControllers();

		throw new Exception("Exception in Startup.ConfigureServices");
	}
}
Code language: C# (cs)

All'avvio di questa app, l'eccezione non gestita provocherà l'attivazione dell'evento UnhandledException, che registra quanto segue:

2021-09-09 15:57:51.6949 ERROR UnhandledExceptionHandler - Exception=System.Exception: Exception in Startup.ConfigureServices
   at ExampleWebApp.Startup.ConfigureServices(IServiceCollection services) in Startup.cs:line 31Code language: plaintext (plaintext)