すべての .NET アプリケーションで使用できる 2 つのグローバル例外イベントがあります:
- FirstChanceException:例外がスローされると、このイベントは何よりも先に発生します。
- UnhandledException:未処理の例外がある場合、このイベントはプロセスが終了する直前に発生します。
次のように、これらのイベント ハンドラーを Main() で (他の処理が実行される前に) 結び付けます。
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)
これにより、クラッシュする前に次のように出力されます:
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 17
Code language: plaintext (plaintext)
最初に発生した FirstChanceException イベントに注意してください。このイベントは、catch であっても、他の何よりも先に発生します ブロック (この例を以下に示します)。 try/catch を必要とする代わりに、これを集中例外ロギングに使用できます。 コード全体に散在する例外をログに記録するためだけのブロック。
この記事では、これらのグローバル例外イベント ハンドラーについて詳しく説明し、WinForms アプリと ASP.NET Core アプリでの使用方法の違いを示します。
例外が処理された FirstChanceException イベント
例外が発生すると、最初に FirstChanceException イベントにルーティングされます。次に、適切な catch ブロックにルーティングされます。
以下に例を示します:
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)
これは以下を出力します:
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 19
Code language: plaintext (plaintext)
これは、FirstChanceException イベントが常に最初に発生することを示しています。
破損状態の例外
破損状態の例外 (アンマネージ コードでのアクセス違反など) によってプログラムがクラッシュし、グローバル例外イベント ハンドラーが起動されません。 .NET Core と .NET Framework では動作が異なります。以下に両方の例を示します。
まず、アクセス違反の例外をスローするコードは次のとおりです:
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 コア
これを .NET Core アプリで実行すると、次の例外が発生します (フレームワークによって書き込まれます):
Fatal error. Internal CLR error. (0x80131506)
at System.Runtime.InteropServices.Marshal.StructureToPtr(System.Object, IntPtr, Boolean)
Code language: plaintext (plaintext)
例外を例外イベント ハンドラにルーティングしません。
.NET フレームワーク
.NET Framework アプリの既定の動作は、.NET Core の動作に似ています。次の例外でクラッシュします:
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)
例外を例外イベント ハンドラーにルーティングしませんでした。ただし、この動作は、HandleProcessCorruptedStateExceptions 属性をメソッドに追加することで変更できます:
[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)
クラッシュする前に例外をイベント ハンドラーにルーティングするようになりました。以下を出力します:
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 15
Code language: plaintext (plaintext)
注:
- この機能は .NET Core で削除されました。 HandleProcessCorruptedStateExceptions 属性を使用しても無視されます。
- コードを変更したくない場合は、legacyCorruptedStateExceptionsPolicy app.config 属性を使用できます。
<?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 には、3 番目のグローバル例外イベントがあります。これは ThreadException と呼ばれます。これは、FirstChanceException や UnhandledException と同様に、Main() で関連付けることができます:
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)
ThreadException イベントは、未処理の例外が WinForms スレッド (クリック イベント ハンドラーなど) で発生したときに発生します。未処理の例外が他の場所で発生した場合は、代わりに UnhandledException イベントが発生します。以下に例を示します。
WinForms スレッドで未処理の例外
コントロール イベント ハンドラー (ボタン クリックなど) は、WinForms スレッドで処理されます。以下は、WinForms スレッドで未処理の例外の例です:
private void btnThrow_Click(object sender, EventArgs e)
{
throw new Exception("btnThrow_Click exception");
}
Code language: C# (cs)
これが何が起こるかです。まず、FirstChanceException イベントが発生します:
FirstChanceExceptionEventHandler - Exception=System.Exception: btnThrow_Click exception...
Code language: plaintext (plaintext)
その後、ThreadException イベントが発生します:
ThreadExceptionEventHandler - Exception=System.Exception: btnThrow_Click exception...
Code language: plaintext (plaintext)
ThreadException イベントを使用せず、WinForms スレッドで未処理の例外が発生した場合、既定の動作では、「未処理の例外が発生しました...」という標準エラー ダイアログ ウィンドウが表示されますが、これは望ましくない場合があります。そのため、ThreadException イベントを使用することをお勧めします。
他の場所で未処理の例外
ThreadException イベントは、例外が WinForms スレッドで発生した場合にのみ発生します。未処理の例外が他の場所で発生した場合、UnhandledException イベントが発生します。
WinForms 以外のスレッドで処理されない例外の 2 つの例を次に示します。
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)
どちらの例でも、FirstChanceException イベントが最初に発生し、その後に UnhandledException イベントが発生します。その後、アプリがクラッシュします。
UnhandledException イベントは、WinForms での致命的な例外のトラブルシューティングに非常に役立ちます。これがないと、処理されない致命的な例外が発生したときに、アプリは何の問題もなくクラッシュします。フォームが描画される前に未処理の例外が発生した場合、何も表示されないため、トラブルシューティングがさらに困難になる可能性があります。
ASP.NET コア
ASP.NET Core アプリで FirstChanceException イベントを使用することはお勧めしません。コントローラーが例外をスローすると、このイベントが繰り返し発生します。
UnhandledException イベントを使用して、次のように起動例外をログに記録できます:
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)
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)
このアプリが起動すると、未処理の例外によって UnhandledException イベントが発生し、次のログが記録されます:
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 31
Code language: plaintext (plaintext)