💡

.NET 10 WinForms/WPF で 共通のグローバル例外ログ基盤を C# と log4net で実装する

に公開

後から思う事・・。まず、はじめに、この項目だけ読んで、当記事全体は読む価値ないと感じたら、読まなくてもいいかと思います

この記事に書いたような、大掛かりなことせんでも、

純粋に、Program.cs にて、

namespace XXX
{
    internal static class Program
    {
        /// <summary>
        ///  The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            Application.ThreadException += (sender, e) =>
            {
                var msg = e.Exception.Message + "\n\n" + e.Exception.StackTrace;
                MessageBox.Show(msg, "Unhandled Thread Exception", MessageBoxButtons.OK, MessageBoxIcon.Error);
            };
            // To customize application configuration such as set high DPI settings or default font,
            // see https://aka.ms/applicationconfiguration.
            ApplicationConfiguration.Initialize();
            Application.Run(new Form1());
        }
    }
}

            Application.ThreadException += (sender, e) =>
            {
                var msg = e.Exception.Message + "\n\n" + e.Exception.StackTrace;
                MessageBox.Show(msg, "Unhandled Thread Exception", MessageBoxButtons.OK, MessageBoxIcon.Error);
            };

に相当する部分だけでした。

NuGetで、log4netをインストールした後、
この中に、例外のスタックトレースをログに出力する実装も、
加えるなどすればとは思うです

この例では、エラーの詳細まで、メッセージボックスで表示しちゃってます
それが、ユーザに見えます。

それが、まずいときも多いです。

エラーがおきたことだけ、簡易的に示すような文言だけ
メッセージボックスで表示しとくのが普通だと思うです

ただ、それだと、エンジニアにとって、何が原因か不明なので、
log4netで、ログに、例外のスタックトレースが
出力されてる状況にしとけばよいと思うです。

単にそれだけなんでしょうけど

この部分だけ、部品化したいようならば、
当記事で説明してるような、Common.Loggingのクラスライブラリを実装するするんだと思うです

それだけの話題です。大したことのない記事です。
log4netが必要になったときとか、調べるの、メンドイし、
この記事からのコピペで、ある程度のところまで、早めに行けるから
初期実装は楽できるのではないか。当記事は、それぐらいの価値しかないです。

0. 前提環境と Visual Studio 2026 での準備(UI 操作手順)

ここでは、Visual Studio Community 2026 日本語版を前提に、この記事のコードをそのまま動かすための
具体的な UI 操作手順をまとめます。

  • IDE:Visual Studio Community 2026(C# / .NET デスクトップ開発ワークロードがインストール済み)
  • .NET:.NET 10(WinForms / WPF プロジェクトが作成できる状態)
  • ログライブラリ:log4net(NuGet 経由で導入)

補足:
NuGet パッケージのインストール方法や、プロジェクト参照の追加方法は、
Visual Studio 2022 以降で統一された UI が使われており、
Visual Studio 2026 でも同じ「右クリック → 参照の追加」「ソリューションの NuGet パッケージの管理」
という流れになっています。

0-1. ソリューション構成(3 プロジェクト)

最終的なソリューション構成は次の 3 つを想定します。

  • 共通クラスライブラリ:Common.Logging(C# クラス ライブラリ / .NET 10 -windows ターゲット)
  • Windows Forms アプリ:MyWinFormsApp(.NET 10 Windows フォーム アプリ)
  • WPF アプリ:MyWpfApp(.NET 10 WPF アプリ)

(1) Common.Logging プロジェクトを追加する

  1. ソリューション エクスプローラーでソリューション(最上位ノード)を右クリック

  2. [追加] → [新しいプロジェクト] を選択

  3. 「プロジェクトの作成」ダイアログで、右上の検索ボックスに クラス ライブラリ と入力

  4. クラス ライブラリ(C# / .NET 用)」テンプレートを選択して [次へ]

  5. プロジェクト名に Common.Logging を入力
    ソリューション名は(空なら)GlobalExceptionLoggingSample など任意で設定

  6. フレームワークに .NET 10 を選択して [作成]

  7. 作成後に Common.Logging.csproj を開き、<TargetFramework> などを次のように書き換えます。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net10.0-windows</TargetFramework>
    <UseWindowsForms>true</UseWindowsForms>
    <UseWPF>true</UseWPF>
  </PropertyGroup>
  
  <ItemGroup>
    <PackageReference Include="log4net" Version="3.2.0" />
  </ItemGroup>
</Project>
  • net10.0 を net10.0-windows に変更する -windows を後ろに追加する
  • <UseWindowsForms>true</UseWindowsForms> を追加する
  • <UseWPF>true</UseWPF> を追加する
    ただし、
  <ItemGroup>
    <PackageReference Include="log4net" Version="3.2.0" />
  </ItemGroup>

の部分は、後述のNuGetでのインストールをすると自動で追加されるので、
この段階で手で追加する必要はない。

補足:Common.Logging では System.Windows.Forms(WinForms)と System.Windows(WPF)の両方を参照するため、
クラス ライブラリ側も Windows 専用 TFM(net10.0-windows)にしておく必要があります。

(2) WinForms プロジェクトを追加する

  1. ソリューション エクスプローラーでソリューション(最上位ノード)を右クリック
  2. [追加] → [新しいプロジェクト] を選択
  3. 検索ボックスに Windows フォーム と入力
  4. Windows フォーム アプリ」テンプレート(C#)を選択し [次へ]
  5. プロジェクト名を MyWinFormsApp として [次へ]
  6. フレームワークに .NET 10 を選択して [作成]

(3) WPF プロジェクトを追加する

  1. ソリューションを右クリック → [追加] → [新しいプロジェクト]
  2. 検索ボックスに WPF と入力
  3. WPF アプリ」テンプレート(C#)を選択し [次へ]
  4. プロジェクト名を MyWpfApp として [次へ]
  5. フレームワークに .NET 10 を選択して [作成]

ここまでで、ソリューション内に
Common.Logging / MyWinFormsApp / MyWpfApp
の 3 プロジェクトが並んでいる状態になります。


0-2. プロジェクト参照を追加する(Common.Logging を WinForms / WPF から使えるようにする)

WinForms / WPF 側から例外ハンドラクラスを呼ぶため、
それぞれのプロジェクトから Common.Loggingプロジェクト参照を追加します。

WinForms → Common.Logging

  1. ソリューション エクスプローラーで MyWinFormsApp を右クリック
  2. [追加] → [プロジェクト参照] または [プロジェクトの依存関係] → [参照の追加] を選択
  3. 左側で「プロジェクト」を選択
  4. 一覧から Common.Logging にチェックを入れる
  5. [OK] をクリック

WPF → Common.Logging

  1. MyWpfApp プロジェクトを右クリック
  2. [追加] → [プロジェクト参照]
  3. Common.Logging にチェックを入れる
  4. [OK]

これで、MyWinFormsApp / MyWpfApp のコードから

using Common.Logging;

という using で共通クラスを参照できるようになります。


0-3. log4net を NuGet でインストールする(UI 操作)

次に、log4net を NuGet から導入します。
ここでは Common.Logging プロジェクトに対してインストールする前提で説明します。

1) UI からインストールする手順

  1. ソリューション エクスプローラーで Common.Logging を右クリック
  2. コンテキストメニューから [NuGet パッケージの管理] を選択
    (もしくは上部メニュー [ツール] → [NuGet パッケージ マネージャー] → [ソリューションの NuGet パッケージの管理] を開き、
    プロジェクトとして Common.Logging を選択)
  3. 上部タブで [参照](Browse)を選択
  4. 検索ボックスに log4net と入力
  5. 発行元が Apache Software Foundation になっている公式 log4net パッケージを選択

    パッケージ詳細のタブに、

    作成者: The Apache Software Foundation
    と記載がある。
  6. 右側の「プロジェクト」で Common.Logging にチェックが入っていることを確認
  7. [インストール] ボタンをクリック

  8. ライセンス確認ダイアログが出たら内容を確認し [同意する] をクリック

これで Common.Logging プロジェクトに log4net の参照が追加されます。
必要に応じて、MyWinFormsApp / MyWpfApp 側にも同じ手順で log4net を入れて構いませんが、
この記事の構成では ログの実処理は Common.Logging に寄せる想定なので、
最低限 Common.Logging だけに入っていれば動作します。

2) パッケージ マネージャー コンソールから入れる場合(参考)

Visual Studio 上部メニューから:

  1. [ツール] → [NuGet パッケージ マネージャー] → [パッケージ マネージャー コンソール]
  2. 画面下部にコンソールが開くので、右上の「既定のプロジェクト」を Common.Logging にする
  3. 次のコマンドを実行
Install-Package log4net

どちらの方法でも結果は同じです。UI 操作に慣れていない場合は、右クリック → [NuGet パッケージの管理] の方が分かりやすいと思います。


0-4. log4net の設定ファイル(app.config / log4net.config)を追加する

この記事のサンプルでは、もっともシンプルな構成として

  • プロジェクト直下に app.config を作るパターン
  • その中に <log4net> セクションを直接書くパターン

を想定しています。

(1) app.config の追加手順(WinForms / WPF いずれも同様)

  1. 例外ログを出したいプロジェクトを右クリック
    (典型的にはアプリ本体 MyWinFormsApp / MyWpfApp のプロジェクト)
  2. [追加] → [新しい項目] を選択
  3. 「新しい項目の追加」ダイアログで、左側から「C# 項目」を選択
  4. 一覧から [アプリケーション構成ファイル]Application Configuration File)を選択
  5. 名前が App.config であることを確認して [追加]

既に App.config / app.config がある場合は、この手順は不要です。

(2) log4net 設定セクションを App.config に追加する

作成された App.config を開き、
次のように <log4net> セクションと <configSections> の設定を追加します。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />
  </configSections>

  <log4net>
    <appender name="RollingFile" type="log4net.Appender.RollingFileAppender">
      <file value="logs\app.log" />
      <appendToFile value="true" />
      <rollingStyle value="Size" />
      <maxSizeRollBackups value="10" />
      <maximumFileSize value="10MB" />
      <staticLogFileName value="true" />
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%date %-5level %logger - %message%newline%exception" />
      </layout>
    </appender>

    <root>
      <level value="INFO" />
      <appender-ref ref="RollingFile" />
    </root>
  </log4net>
</configuration>

logs フォルダが存在しない場合は、実行時に自動作成されますが、
事前にプロジェクト直下にフォルダを作っておいても構いません。

(3) XmlConfigurator 属性を指定する

log4net に対して「設定ファイルをここから読み込め」と教えるため、
アセンブリ属性として XmlConfigurator を指定します。

手順(.NET 10 プロジェクトの場合の一例):

  1. Common.Logging プロジェクトを右クリック → [追加] → [新しい項目]
  2. クラス」ではなく、「アセンブリ情報ファイル」がテンプレートにあればそれを選択
    (ない場合はクラスを 1 つ作り、そこで属性を書いても構いません)
  3. ここでは例として AssemblyInfo.cs に次の 1 行を追加します。
using log4net.Config;

[assembly: XmlConfigurator(Watch = true)]

ConfigFile = "log4net.config" のように別ファイルを指定する構成もありますが、
この記事の例では App.config 内の <log4net> セクションを読む前提なので、
ConfigFile 指定は省略しています。


0-5. Common.Logging のコードを貼り付ける

このあと全文を掲載する「ベース記事」の Common.Logging 側コード(

  • GlobalExceptionLogger
  • WinFormsGlobalExceptionHandler
  • WpfGlobalExceptionHandler

)を、そのまま Common.Logging プロジェクトにクラスとして追加します。

  1. Common.Logging プロジェクトを右クリック → [追加] → [クラス]
  2. 名前を GlobalExceptionLogger.cs にして作成 → ベース記事中のコードをそのまま貼り付け
  3. 同様に WinFormsGlobalExceptionHandler.cs / WpfGlobalExceptionHandler.cs を追加し、
    それぞれのコードを貼り付け

貼り付け先の名前空間は、記事の例では Common.Logging になっています。
プロジェクトの既定名前空間も Common.Logging に合わせておくと、そのまま動きます。


0-6. WinForms / WPF 側から Initialize() を呼び出す

最後に、アプリ側のエントリポイントから、共通ハンドラの Initialize() を 1 回だけ呼び出します。

WinForms(MyWinFormsApp / Program.cs)

using System;
using System.Windows.Forms;
using Common.Logging;

internal static class Program
{
    [STAThread]
    static void Main()
    {
        // 1) グローバル例外ハンドラの初期化
        WinFormsGlobalExceptionHandler.Initialize();

        // 2) 通常の WinForms 初期化
        ApplicationConfiguration.Initialize();

        // 3) メインフォーム実行
        Application.Run(new Form1());
    }
}

この Program.cs は、Visual Studio で WinForms アプリを作成したときに自動生成されるファイルに、
WinFormsGlobalExceptionHandler.Initialize(); の 1 行を追加するイメージです。

WPF(MyWpfApp / App.xaml.cs)

using System.Windows;
using Common.Logging;

namespace MyWpfApp
{
    public partial class App : Application
    {
        public App()
        {
            // Application インスタンス自身を渡して初期化
            WpfGlobalExceptionHandler.Initialize(this);
        }
    }
}

こちらも、Visual Studio が自動生成した App.xaml.cs に 1 行追加するだけです。


0-7. 動作確認のポイント

  1. 任意のイベントで、わざと未処理例外を投げるコードを入れる
    例:ボタンクリック時に throw new InvalidOperationException("テスト例外");
  2. アプリを実行し、そのボタンを押してアプリをクラッシュさせる
  3. プロジェクト直下(実行 EXE のカレントディレクトリ)に logs/app.log ができているか確認
  4. app.log を開き、Unhandled exception in ... のメッセージと stack trace が出ていることを確認

ここまで動けば、この記事の「ベース記事」で説明している設計(共通ライブラリ+グローバル例外フック)が、
Visual Studio Community 2026 / .NET 10 の環境で実際に動いていると言えます。


この記事のゴール

.NET 10 の Windows デスクトップアプリ(Windows Forms / WPF)で、

  • catch していない例外(未処理例外)を、すべて 1 か所のログ出力に集約する
  • ログ出力には log4net を使う
  • 各アプリケーションごとにバラバラに実装するのではなく、
    共通クラスライブラリ(Common プロジェクト)に例外ハンドリング処理をまとめる

という構成を、実際に貼り付けて使えるコード付きでまとめます。

結論から言うと、

  • log4net の設定ファイルが正しく置かれている
  • 参照設定(プロジェクト参照・NuGet)が正しい

という前提が満たされていれば、この記事のコードは WinForms / WPF 双方で問題なく動作する構成になっています。


全体像:どのイベントで「未処理例外」を拾うか

.NET デスクトップアプリでは、「catch していない例外」を最後の砦として拾うイベントが複数あります。

WinForms

  • Application.ThreadException
    • Windows Forms の UI スレッドで処理されなかった例外ごとに発生するイベント。
  • AppDomain.CurrentDomain.UnhandledException
    • 非 UI スレッドやバックグラウンド処理など、アプリケーションドメイン全体での未処理例外を通知するイベント。
  • TaskScheduler.UnobservedTaskException
    • Task / Task<T> で発生した例外のうち、await / Wait などで観測されず GC 時に収集されるタイミングで通知されるイベント。

WPF

  • Application.DispatcherUnhandledException
    • WPF アプリのメイン UI スレッド(Dispatcher)で処理されなかった例外ごとに発生するイベント。
  • AppDomain.CurrentDomain.UnhandledException
  • TaskScheduler.UnobservedTaskException

つまり、

  • WinForms では Application.ThreadException
  • WPF では Application.DispatcherUnhandledException
  • 共通部分として AppDomain.CurrentDomain.UnhandledExceptionTaskScheduler.UnobservedTaskException

をフックすれば、

「catch していない例外を 1 か所のログ出力(log4net)に集約する」

という目的は十分に達成できます。


設計方針

設計をシンプルに保つため、次のような方針にします。

  1. Common.Logging というクラスライブラリプロジェクトを作る
    • log4net に対して実際にログを書き込むのは Common 側の責務。
  2. Common.Logging には 3 つのクラスを用意する
    • GlobalExceptionLogger
      → 例外オブジェクトと「どこで発生したか」の文字列を受け取り、log4net に書き込む。
    • WinFormsGlobalExceptionHandler
      → WinForms アプリの Program.Main から 1 回だけ呼び出して、
      Application.ThreadException / AppDomain.CurrentDomain.UnhandledException /
      TaskScheduler.UnobservedTaskException をまとめて登録する。
    • WpfGlobalExceptionHandler
      → WPF アプリの App.xaml.cs から 1 回だけ呼び出して、
      Application.DispatcherUnhandledException / AppDomain.CurrentDomain.UnhandledException /
      TaskScheduler.UnobservedTaskException をまとめて登録する。
  3. 各アプリ側(WinForms / WPF)は、入口で Initialize() を 1 回呼ぶだけにする

こうすることで、

  • 例外ログの実装は Common.Logging 側に集中
  • 各アプリ側のコード変更は最小限(Program.cs / App.xaml.cs の数行だけ)
  • WinForms / WPF のどちらでも同じパターンを使い回せる

という状態を作ります。


1. Common.Logging プロジェクトの実装

まずはクラスライブラリプロジェクト Common.Logging を作成し、log4net を NuGet で追加します。

dotnet add package log4net

ここでは、

  • GlobalExceptionLogger
  • WinFormsGlobalExceptionHandler
  • WpfGlobalExceptionHandler

の 3 クラスを定義します。


1-1. GlobalExceptionLogger:log4net に例外を流す共通クラス

using log4net;

namespace XXPro.Common.Logging
{
    /// <summary>
    /// グローバル例外のログ出力とユーザー通知(エラーダイアログ)を一元化するクラス
    /// </summary>
    public static class GlobalExceptionLogger
    {
        // log4net の Logger(クラス単位で 1 つ)
        private static readonly ILog Logger =
            LogManager.GetLogger(typeof(GlobalExceptionLogger));

        /// <summary>
        /// 指定された context が「このあとアプリケーションが終了する経路」かどうかを判定する。
        /// </summary>
        private static bool IsTerminatingContext(string context)
        {
            return context switch
            {
                // WinForms UI スレッドは Application.ThreadException でハンドルされ、アプリは継続
                "WinForms UI thread (Application.ThreadException)" => false,

                // AppDomain.CurrentDomain.UnhandledException 経由は必ずアプリケーションが終了する
                "WinForms non-UI thread (AppDomain.CurrentDomain.UnhandledException)" => true,
                "WPF non-UI thread (AppDomain.CurrentDomain.UnhandledException)" => true,

                // 現状の実装では DispatcherUnhandledException で e.Handled を true にしていないため、
                // 最終的に AppDomain.CurrentDomain.UnhandledException へ流れてアプリケーションが終了する
                "WPF UI thread (Application.DispatcherUnhandledException)" => true,

                // TaskScheduler.UnobservedTaskException はハンドラ内で e.SetObserved() を呼んでいるため、
                // 既定の例外エスカレーション(プロセス終了)は発生しない
                "TaskScheduler.UnobservedTaskException" => false,

                // 想定外の context は安全側に倒して「終了する」と扱う
                _ => true,
            };
        }

        /// <summary>
        /// グローバル例外をログに出力し、エラーダイアログも表示する。
        /// log4net 側の設定(app.config / log4net.config)は別途済んでいる前提。
        /// </summary>
        public static void Log(Exception? exception, string context)
        {
            bool willExit = IsTerminatingContext(context);

            if (exception == null)
            {
                Logger.Error($"Unhandled exception (null) in {context}");

                string message = willExit
                    ? "エラーが発生しました。アプリケーションを終了します。"
                    : "エラーが発生しました。(アプリケーションは継続します)";

                ShowErrorDialog(message);
            }
            else
            {
                // Error(message, exception) は stack trace 付きでログ出力される
                Logger.Error($"Unhandled exception in {context}", exception);

                string header = willExit
                    ? "エラーが発生しました。アプリケーションを終了します。"
                    : "エラーが発生しました。(アプリケーションは継続します)";

                string message =
                    header + "\r\n\r\n"
                    + $"発生箇所: {context}\r\n"
                    + $"メッセージ: {exception.Message}";

                ShowErrorDialog(message);
            }
        }

        /// <summary>
        /// エラーアイコン付きのメッセージボックスを表示する共通メソッド
        /// </summary>
        private static void ShowErrorDialog(string message)
        {
            try
            {
                MessageBox.Show(
                    message,
                    "エラー",
                    MessageBoxButtons.OK,
                    MessageBoxIcon.Error);
            }
            catch
            {
                // ここでさらに例外が出てもどうしようもないので握りつぶす
            }
        }
    }
}

GlobalExceptionLogger のコード解説(初心者向け)

  • public static class GlobalExceptionLogger
    • アプリ全体で 1 つだけ使う「例外ログの窓口」です。static クラスなので、毎回 new せずに GlobalExceptionLogger.Log(...) のように直接呼び出せます。
  • private static readonly ILog Logger = LogManager.GetLogger(typeof(GlobalExceptionLogger));
    • log4net が提供する ILog インターフェイスのインスタンスを 1 個だけ作って保持しています。LogManager.GetLogger(...) でクラスごとのロガーを取得し、このロガー経由でファイルなどにログが書き出されます。
  • private static bool IsTerminatingContext(string context)
    • どのイベント(Application.ThreadException / AppDomain.CurrentDomain.UnhandledException / TaskScheduler.UnobservedTaskException など)から来た例外なのかを表す context 文字列を受け取り、「このあとアプリケーションが終了する経路かどうか」を true/false で判定します。
    • ここで true を返すケースでは、メッセージダイアログに「アプリケーションを終了します」と表示します。false の場合は「アプリケーションは継続します」と表示し、ユーザーに「落ちるかどうか」が明確に伝わるようにしています。
  • public static void Log(Exception? exception, string context)
    • WinForms / WPF のすべてのグローバル例外ハンドラから呼び出される「共通処理の本体」です。
      大まかな流れは次の 3 ステップです。
      1. bool willExit = IsTerminatingContext(context); で、このあとアプリが終了するかどうかを判定する
      2. Logger.Error(...) で、例外情報を log4net に書き出す
      3. ShowErrorDialog(...) で、ユーザーにエラーダイアログを表示する
    • exception == null の場合も考慮しており、そのときはスタックトレースなしのシンプルなエラーメッセージを表示するようになっています。
  • private static void ShowErrorDialog(string message)
    • 実際に MessageBox.Show を呼び出す小さな共通メソッドです。
      WinForms / WPF のどちらから呼ばれても使えるように、常に「OK ボタン 1 つ」「エラーアイコン付き」のメッセージボックスとして固定しています。

1-2. WinFormsGlobalExceptionHandler:WinForms 用のグローバル例外ハンドラ

namespace XXPro.Common.Logging
{
    /// <summary>
    /// Windows Forms アプリで未処理例外を一括ログするための初期化クラス
    /// </summary>
    public static class WinFormsGlobalExceptionHandler
    {
        private static bool _initialized;

        /// <summary>
        /// Program.Main から 1 回だけ呼び出す。
        /// </summary>
        public static void Initialize()
        {
            if (_initialized) return;
            _initialized = true;

            // UI スレッドの未処理例外を Application.ThreadException に流す設定
            // (最初のウィンドウを作る前に呼ぶ必要がある)
            Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);

            // 1) UI スレッドの未処理例外
            Application.ThreadException += (sender, e) =>
            {
                GlobalExceptionLogger.Log(
                    e.Exception,
                    "WinForms UI thread (Application.ThreadException)");
            };

            // 2) 非 UI スレッド・プロセスレベルの未処理例外
            AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
            {
                var ex = e.ExceptionObject as Exception;
                GlobalExceptionLogger.Log(
                    ex,
                    "WinForms non-UI thread (AppDomain.CurrentDomain.UnhandledException)");
            };

            // 3) 未観測 Task の例外
            TaskScheduler.UnobservedTaskException += (sender, e) =>
            {
                GlobalExceptionLogger.Log(
                    e.Exception,
                    "TaskScheduler.UnobservedTaskException");
                e.SetObserved(); // 既定の「プロセス終了」ポリシーを防ぐ
            };
        }
    }
}

WinFormsGlobalExceptionHandler のコード解説(初心者向け)

  • このクラスの役割
    • Program.Main から WinFormsGlobalExceptionHandler.Initialize(); を 1 回呼ぶだけで、
      • Application.ThreadException(UI スレッド)
      • AppDomain.CurrentDomain.UnhandledException(非 UI スレッド・プロセスレベル)
      • TaskScheduler.UnobservedTaskException(未観測タスクの例外)
        に対するグローバル例外ハンドラを一括で登録し、最終的にすべて GlobalExceptionLogger.Log(...) に流す「初期化専用クラス」です。
  • private static bool _initialized;if (_initialized) return;
    • Initialize() が誤って複数回呼ばれても同じイベントに重複登録されないようにするためのフラグです。
      .NET のイベントは += を繰り返すとその分だけハンドラが増えてしまうため、1 回だけ登録する仕組みを用意しています。
  • Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
    • WinForms では、これを最初のフォームを作る前に呼ぶと「UI スレッドで発生した未処理例外を Application.ThreadException イベントに流す」モードになります。
      これにより、UI 例外をすべて共通の場所で捕まえられるようになります。
  • Application.ThreadException += (sender, e) => { ... };
    • WinForms の UI スレッド上で catch されなかった例外がこのイベントに流れてきます。
      ここでは e.Exception"WinForms UI thread (Application.ThreadException)" という context 文字列を GlobalExceptionLogger.Log に渡し、「UI スレッドで起きた例外」としてログとダイアログを出しています。
  • AppDomain.CurrentDomain.UnhandledException += (sender, e) => { ... };
    • バックグラウンドスレッドなど、UI 以外で発生した未処理例外はこのイベントで通知されます。
      e.ExceptionObjectException 型とは限らないため、as Exception でキャストした上で GlobalExceptionLogger.Log に渡しています。
  • TaskScheduler.UnobservedTaskException += (sender, e) => { ... };
    • Task / Task<T> の例外で、await / Wait / Result などで誰にも観測されないまま GC されそうになったときに発生するイベントです。
    • ここでも GlobalExceptionLogger.Log に例外を流したあと、e.SetObserved(); を呼ぶことで「この例外はハンドル済み」とマークし、.NET の既定の「プロセス終了」挙動を止めています。

1-3. WpfGlobalExceptionHandler:WPF 用のグローバル例外ハンドラ

namespace XXPro.Common.Logging
{
    /// <summary>
    /// WPF アプリで未処理例外を一括ログするための初期化クラス
    /// </summary>
    public static class WpfGlobalExceptionHandler
    {
        private static bool _initialized;

        /// <summary>
        /// App.xaml.cs のコンストラクタか OnStartup から 1 回だけ呼び出す。
        /// </summary>
        public static void Initialize(System.Windows.Application app)
        {
            if (_initialized) return;
            _initialized = true;

            if (app == null) throw new ArgumentNullException(nameof(app));

            // 1) UI スレッド(Dispatcher)の未処理例外
            app.DispatcherUnhandledException += (sender, e) =>
            {
                GlobalExceptionLogger.Log(
                    e.Exception,
                    "WPF UI thread (Application.DispatcherUnhandledException)");

                // アプリを落としたくない場合は、ポリシーに応じてここで e.Handled = true; を検討
                // e.Handled = true;
            };

            // 2) 非 UI スレッド・プロセスレベルの未処理例外
            AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
            {
                var ex = e.ExceptionObject as Exception;
                GlobalExceptionLogger.Log(
                    ex,
                    "WPF non-UI thread (AppDomain.CurrentDomain.UnhandledException)");
            };

            // 3) 未観測 Task の例外
            TaskScheduler.UnobservedTaskException += (sender, e) =>
            {
                GlobalExceptionLogger.Log(
                    e.Exception,
                    "TaskScheduler.UnobservedTaskException");
                e.SetObserved();
            };
        }
    }
}

WpfGlobalExceptionHandler のコード解説(初心者向け)

  • このクラスの役割
    • App.xaml.cs のコンストラクタや OnStartup から WpfGlobalExceptionHandler.Initialize(this); を 1 回だけ呼ぶことで、
      • Application.DispatcherUnhandledException(WPF の UI スレッド/Dispatcher)
      • AppDomain.CurrentDomain.UnhandledException(非 UI スレッド・プロセスレベル)
      • TaskScheduler.UnobservedTaskException(未観測タスクの例外)
        に対するグローバル例外ハンドラをまとめて登録し、すべて GlobalExceptionLogger.Log(...) に流すクラスです。
  • public static void Initialize(System.Windows.Application app)app 引数
    • ここで受け取っている app は WPF アプリケーションのインスタンスそのものです。
      app.DispatcherUnhandledException にアクセスするためには、このインスタンスが必須なので、null で渡された場合は ArgumentNullException を投げて明示的にエラーにしています。
  • app.DispatcherUnhandledException += (sender, e) => { ... };
    • WPF の UI スレッド(Dispatcher)で catch されなかった例外がこのイベントに流れてきます。
      ここでは GlobalExceptionLogger.Log に例外を渡しつつ、context として "WPF UI thread (Application.DispatcherUnhandledException)" を指定しています。
    • コメントにある通り、「UI 例外が起きてもアプリを落としたくない」ポリシーにしたい場合は、ここで e.Handled = true; を設定することで、例外発生後もアプリを継続させることができます。
  • AppDomain.CurrentDomain.UnhandledException += (sender, e) => { ... };
    • WPF でもバックグラウンドスレッドなど UI 以外で発生した未処理例外は、このイベントで通知されます。
      WinForms の場合と同様に、e.ExceptionObject as Exception でキャストしてから GlobalExceptionLogger.Log に渡しています。
  • TaskScheduler.UnobservedTaskException += (sender, e) => { ... };
    • 非同期タスクの未観測例外を拾うためのイベントです。WinForms 側と同じように、GlobalExceptionLogger.Log でログとダイアログを出したあと、e.SetObserved(); で既定のプロセス終了を防いでいます。

2. 各アプリ側からの呼び出し

2-1. Windows Forms アプリ側(Program.cs)

using System;
using System.Windows.Forms;
using Common.Logging;

internal static class Program
{
    [STAThread]
    static void Main()
    {
        // 1) グローバル例外ハンドラの初期化
        WinFormsGlobalExceptionHandler.Initialize();

        // 2) 通常の WinForms 初期化
        ApplicationConfiguration.Initialize();

        // 3) メインフォーム実行
        Application.Run(new Form1());
    }
}

Program.cs のコード解説(初心者向け)

  • internal static class Program
    • WinForms アプリのエントリポイント(入り口)を定義するクラスです。Visual Studio のテンプレートで自動生成される Program クラスをベースにしています。
  • [STAThread] 属性
    • Windows の UI コンポーネント(WinForms や WPF)は「シングルスレッドアパートメント(STA)」というモードで動くことが前提なので、エントリポイントの Main メソッドにはこの属性が必須です。
  • WinFormsGlobalExceptionHandler.Initialize();
    • 自作した WinFormsGlobalExceptionHandler の初期化を、アプリ起動直後に 1 回だけ呼び出しています。
      ここでグローバル例外ハンドラのイベント登録が行われるため、以降の処理で発生した未処理例外はすべて共通の GlobalExceptionLogger.Log に流れます。
  • ApplicationConfiguration.Initialize(); / Application.EnableVisualStyles(); など(テンプレートに応じて)
    • ここでは WinForms アプリの見た目や高 DPI 設定などを初期化します。Visual Studio 2022 以降では ApplicationConfiguration.Initialize(); というメソッドにまとめられている場合もあります。
  • Application.Run(new Form1());
    • 実際にメインフォーム(ここでは Form1)を作成し、そのフォームが閉じられるまでメッセージループを回し続けるメソッドです。
      この呼び出し以降に UI スレッドで起きた未処理例外は、WinFormsGlobalExceptionHandler.Initialize で登録した Application.ThreadException から GlobalExceptionLogger に渡されます。

2-2. WPF アプリ側(App.xaml.cs)

using System.Windows;
using Common.Logging;

namespace MyWpfApp
{
    public partial class App : Application
    {
        public App()
        {
            // Application インスタンス自身を渡して初期化
            WpfGlobalExceptionHandler.Initialize(this);
        }
    }
}

App.xaml.cs のコード解説(初心者向け)

  • public partial class App : Application
    • WPF アプリケーション全体を表すクラスです。XAML 側の App.xaml と対になっており、アプリのライフサイクル(起動・終了など)を管理します。
  • public App() コンストラクタ
    • アプリケーションインスタンスが作成された直後に 1 回だけ呼ばれます。
      ここで WpfGlobalExceptionHandler.Initialize(this); を呼び出すことで、WPF 全体のグローバル例外ハンドラをセットアップしています。
  • OnStartup を使う場合のイメージ
    • コンストラクタではなく protected override void OnStartup(StartupEventArgs e) をオーバーライドして、その中で WpfGlobalExceptionHandler.Initialize(this); を呼び出す書き方もよく使われます。
      いずれの場合も、「アプリのウィンドウを開く前に 1 回だけ初期化する」という点が重要です。

3. log4net 側の設定のイメージ

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" />
  </configSections>

  <log4net>
    <appender name="RollingFile" type="log4net.Appender.RollingFileAppender">
      <file value="logs\app.log" />
      <appendToFile value="true" />
      <rollingStyle value="Size" />
      <maxSizeRollBackups value="10" />
      <maximumFileSize value="10MB" />
      <staticLogFileName value="true" />
      <layout type="log4net.Layout.PatternLayout">
        <conversionPattern value="%date %-5level %logger - %message%newline%exception" />
      </layout>
    </appender>

    <root>
      <level value="INFO" />
      <appender-ref ref="RollingFile" />
    </root>
  </log4net>
</configuration>
using log4net.Config;

[assembly: XmlConfigurator(Watch = true)]

4. 注意点・限界

  • すべてのケースで「アプリが落ちない」ことを保証するものではない。
    • AppDomain.CurrentDomain.UnhandledException はプロセス終了直前の通知イベントであり、
      ここでログを書いてもアプリ自体は終了するケースがある。
  • TaskScheduler.UnobservedTaskException は GC やファイナライザのタイミングに依存して発火する。
  • WPF の DispatcherUnhandledExceptione.Handled を true にするかどうかは、
    アプリ側のポリシーに応じて決める必要がある。

5. まとめ

  • WinForms / WPF で「catch していない例外を 1 か所で log4net に流す」には、
    • WinForms:Application.ThreadException
    • WPF:Application.DispatcherUnhandledException
    • 共通:AppDomain.CurrentDomain.UnhandledException / TaskScheduler.UnobservedTaskException
      をフックする構成が実務的な標準パターン。
  • これらのイベント登録と log4net へのログ出力を Common.Logging にまとめてしまえば、
    各アプリ側は Initialize() を 1 回呼ぶだけで済む。
  • 本記事のコードは、
    • Common.Logging プロジェクト
    • WinForms プロジェクト
    • WPF プロジェクト
      に貼り付けていけば、そのまま動作する構成になっている。

ログの出力先やメッセージ内容、アプリ継続可否のポリシーなどは、
各プロジェクトごとに GlobalExceptionLogger や各ハンドラクラス内で調整していく形になります。

Discussion