C# – Globale Ereignishandler für Ausnahmen

C# – Globale Ereignishandler für Ausnahmen

In allen .NET-Anwendungen sind zwei globale Ausnahmeereignisse verfügbar:

  • FirstChanceException:Wenn eine Ausnahme ausgelöst wird, wird dieses Ereignis vor allen anderen ausgelöst.
  • UnhandledException:Wenn es eine unbehandelte Ausnahme gibt, wird dieses Ereignis unmittelbar vor dem Beenden des Prozesses ausgelöst.

Sie verbinden diese Event-Handler in Main() (bevor irgendetwas anderes ausgeführt wird), wie folgt:

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)

Dies gibt vor dem Absturz Folgendes aus:

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)

Beachten Sie das zuerst ausgelöste FirstChanceException-Ereignis. Dieses Ereignis wird vor allem anderen ausgelöst, sogar catch Blöcke (ein Beispiel dafür zeige ich weiter unten). Sie können dies für die zentralisierte Protokollierung von Ausnahmen verwenden, anstatt try/catch zu benötigen Blöcke nur zum Protokollieren von Ausnahmen, die im gesamten Code verstreut sind.

In diesem Artikel gehe ich näher auf diese globalen Ausnahmeereignishandler ein und zeige dann, wie sie in WinForms- und ASP.NET Core-Apps unterschiedlich verwendet werden.

Das FirstChanceException-Ereignis mit behandelten Ausnahmen

Wenn eine Ausnahme auftritt, wird sie zuerst an das FirstChanceException-Ereignis weitergeleitet. Dann wird es an den entsprechenden Catch-Block weitergeleitet.

Hier ist ein Beispiel:

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)

Dies gibt Folgendes aus:

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)

Dies zeigt, dass das FirstChanceException-Ereignis immer zuerst ausgelöst wird.

Beschädigte Zustandsausnahmen

Beschädigte Zustandsausnahmen (z. B. Zugriffsverletzungen in nicht verwaltetem Code) führen zum Absturz des Programms, und die Ereignishandler für globale Ausnahmen werden nicht ausgelöst. Das Verhalten unterscheidet sich zwischen .NET Core und .NET Framework. Ich werde unten Beispiele für beides zeigen.

Zunächst ist hier der Code, der eine Ausnahme bei einer Zugriffsverletzung auslöst:

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

Die Ausführung in einer .NET Core-App führt zu der folgenden Ausnahme (vom Framework geschrieben):

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

Es leitet die Ausnahme nicht an die Ereignishandler für Ausnahmen weiter.

.NET Framework

Das Standardverhalten in einer .NET Framework-App ähnelt dem .NET Core-Verhalten. Es stürzt mit der folgenden Ausnahme ab:

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)

Die Ausnahme wurde nicht an die Ausnahmeereignishandler weitergeleitet. Dieses Verhalten kann jedoch geändert werden, indem den Methoden das Attribut HandleProcessCorruptedStateExceptions hinzugefügt wird:

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

Jetzt leitet es die Ausnahme an die Ereignishandler weiter, bevor es abstürzt. Es gibt Folgendes aus:

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)

Hinweise:

  • Diese Funktionalität wurde in .NET Core entfernt. Selbst wenn Sie das HandleProcessCorruptedStateExceptions-Attribut verwenden, wird es ignoriert.
  • Sie können das app.config-Attribut „legacyCorruptedStateExceptionsPolicy“ verwenden, wenn Sie den Code nicht ändern möchten.
<?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 haben ein drittes globales Ausnahmeereignis. Es heißt ThreadException. Dies kann in Main() verdrahtet werden, genau wie FirstChanceException und 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)

Das ThreadException-Ereignis wird ausgelöst, wenn eine nicht behandelte Ausnahme in einem WinForms-Thread auftritt (z. B. in einem Click-Ereignishandler). Wenn irgendwo anders eine unbehandelte Ausnahme auftritt, wird stattdessen das UnhandledException-Ereignis ausgelöst. Ich werde unten Beispiele zeigen.

Unbehandelte Ausnahme in einem WinForms-Thread

Steuerungsereignishandler (wie Schaltflächenklicks) werden in WinForms-Threads behandelt. Hier ist also ein Beispiel für eine unbehandelte Ausnahme in einem WinForms-Thread:

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

Hier ist, was passiert. Zuerst wird das FirstChanceException-Ereignis ausgelöst:

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

Dann wird das ThreadException-Ereignis ausgelöst:

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

Wenn Sie das ThreadException-Ereignis nicht verwenden und eine unbehandelte Ausnahme in einem WinForms-Thread auftritt, ist das Standardverhalten, dass das Standardfehlerdialogfenster mit der Meldung „Unbehandelte Ausnahme ist aufgetreten …“ angezeigt wird, was manchmal unerwünscht ist. Deshalb ist es eine gute Idee, das ThreadException-Ereignis zu verwenden.

Unbehandelte Ausnahme an anderer Stelle

Das ThreadException-Ereignis wird nur ausgelöst, wenn die Ausnahme in einem WinForms-Thread aufgetreten ist. Wenn irgendwo anders eine unbehandelte Ausnahme auftritt, wird das UnhandledException-Ereignis ausgelöst.

Hier sind zwei Beispiele für nicht behandelte Ausnahmen in Nicht-WinForms-Threads:

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 beiden Beispielen wird zuerst das FirstChanceException-Ereignis ausgelöst, gefolgt vom UnhandledException-Ereignis. Dann stürzt die App ab.

Das UnhandledException-Ereignis kann sehr nützlich sein, um schwerwiegende Ausnahmen in WinForms zu beheben. Ohne dies stürzt die App ab, wenn eine schwerwiegende, nicht behandelte Ausnahme auftritt, ohne dass ein Hinweis auf ein Problem vorliegt. Wenn eine nicht behandelte Ausnahme auftritt, bevor das Formular gezeichnet wird, kann die Fehlerbehebung noch schwieriger sein, da Sie überhaupt nichts sehen.

ASP.NET Core

Ich würde nicht vorschlagen, das FirstChanceException-Ereignis in einer ASP.NET Core-App zu verwenden. Wenn Controller Ausnahmen auslösen, wird dieses Ereignis wiederholt ausgelöst.

Sie können das UnhandledException-Ereignis verwenden, um Startausnahmen wie folgt zu protokollieren:

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)

Nehmen wir an, es gibt eine unbehandelte Ausnahme 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)

Wenn diese App gestartet wird, löst die unbehandelte Ausnahme das UnhandledException-Ereignis aus, das Folgendes protokolliert:

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)