C#:controladores de eventos de excepción global

C#:controladores de eventos de excepción global

Hay dos eventos de excepción global disponibles en todas las aplicaciones .NET:

  • FirstChanceException:cuando se lanza una excepción, este evento se activa antes que cualquier otro.
  • Excepción no controlada:cuando hay una excepción no controlada, este evento se activa justo antes de que finalice el proceso.

Conecta estos controladores de eventos en Main() (antes de que se haya ejecutado cualquier otra cosa), así:

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)

Esto genera lo siguiente antes de fallar:

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)

Observe que el evento FirstChanceException se disparó primero. Este evento se dispara antes que todo lo demás, incluso catch bloques (abajo mostraré un ejemplo de esto). Puede usar esto para el registro de excepciones centralizado, en lugar de necesitar try/catch bloques solo para registrar excepciones dispersas por todo el código.

En este artículo, entraré en más detalles sobre estos controladores de eventos de excepción global y luego mostraré cómo se usan de manera diferente en las aplicaciones WinForms y ASP.NET Core.

El evento FirstChanceException con excepciones manejadas

Cuando ocurre una excepción, primero se enruta al evento FirstChanceException. Luego se enruta al bloque catch apropiado.

He aquí un ejemplo:

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)

Esto genera lo siguiente:

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)

Esto muestra que el evento FirstChanceException siempre se activa primero.

Excepciones de estado corrupto

Las excepciones de estado dañado (como las infracciones de acceso en código no administrado) bloquean el programa y los controladores de eventos de excepción global no se activan. El comportamiento es diferente entre .NET Core y .NET Framework. Mostraré ejemplos de ambos a continuación.

Primero, aquí hay un código que lanza una excepción de violación de acceso:

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 Núcleo

Ejecutar esto en una aplicación .NET Core da como resultado la siguiente excepción (escrita por el marco):

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

No enruta la excepción a los controladores de eventos de excepción.

Marco .NET

El comportamiento predeterminado en una aplicación de .NET Framework es similar al comportamiento de .NET Core. Se bloquea con la siguiente excepción:

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)

No enrutó la excepción a los controladores de eventos de excepción. Sin embargo, este comportamiento se puede cambiar agregando el atributo HandleProcessCorruptedStateExceptions a los métodos:

[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)

Ahora enruta la excepción a los controladores de eventos antes de fallar. Produce lo siguiente:

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)

Notas:

  • Esta funcionalidad se eliminó en .NET Core. Incluso si usa el atributo HandleProcessCorruptedStateExceptions, se ignorará.
  • Puede usar el atributo app.config legacyCorruptedStateExceptionsPolicy si no desea modificar el código.
<?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 tiene un tercer evento de excepción global. Se llama ThreadException. Esto se puede conectar en Main(), al igual que FirstChanceException y 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)

El evento ThreadException se activa cuando ocurre una excepción no controlada en un subproceso de WinForms (como en un controlador de eventos de clic). Si ocurre una excepción no controlada en cualquier otro lugar, activa el evento UnhandledException en su lugar. Mostraré ejemplos a continuación.

Excepción no controlada en un hilo de WinForms

Los controladores de eventos de control (como los clics de botones) se manejan en subprocesos de WinForms. Así que aquí hay un ejemplo de una excepción no controlada en un hilo de WinForms:

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

Esto es lo que sucede. Primero, se activa el evento FirstChanceException:

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

Entonces se activa el evento ThreadException:

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

Cuando no utiliza el evento ThreadException y ocurre una excepción no controlada en un subproceso de WinForms, el comportamiento predeterminado es que muestra la ventana de diálogo de error estándar que indica "Se ha producido una excepción no controlada...", lo que a veces no es deseable. Por eso es una buena idea usar el evento ThreadException.

Excepción no controlada en cualquier otro lugar

El evento ThreadException solo se activa si la excepción ocurrió en un subproceso de WinForms. Si ocurre una excepción no controlada en cualquier otro lugar, activa el evento UnhandledException.

Aquí hay dos ejemplos de excepciones no controladas en subprocesos que no son de 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)

En ambos ejemplos, el evento FirstChanceException se activa primero, seguido del evento UnhandledException. Entonces la aplicación falla.

El evento UnhandledException puede ser realmente útil para solucionar problemas de excepciones fatales en WinForms. Sin esto, cuando ocurre una excepción fatal no controlada, la aplicación se bloquea sin ningún indicio de problema. Si ocurre una excepción no controlada antes de que se pinte el formulario, puede ser aún más difícil solucionar el problema, porque no verá nada en absoluto.

ASP.NET Core

No sugeriría usar el evento FirstChanceException en una aplicación ASP.NET Core. Cuando los controladores generan excepciones, este evento se activa repetidamente.

Puede usar el evento UnhandledException para registrar excepciones de inicio, como esta:

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)

Digamos que hay una excepción no controlada en 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)

Cuando se inicia esta aplicación, la excepción no controlada hará que se active el evento UnhandledException, que registra lo siguiente:

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)