C#定石 - ログファイル出力

## はじめに

C# ソフト開発時に、決まり事として実施していた内容を記載します。

### テスト環境

ここに記載した情報/ソースコードは、Visual Studio Community 2022 を利用した下記プロジェクトで生成したモジュールを Windows 11 24H2 で動作確認しています。
- Windows Forms - .NET Framework 4.8
- Windows Forms - .NET 8
- WPF - .NET Framework 4.8
- WPF - .NET 8

::: note
Visual Studio 2022 - .NET Framework 4.8 は、C# 7.3 が既定です。
このため、サンプルコードは、C# 7.3 機能範囲で記述しています。
:::

RDP評価は、Windows Server 2025 評価版を利用しました。
Windows Server では「管理用リモートデスクトップ」として同時２セッションまでのリモートデスクトップ接続が許可されています。
上記は、RDS (Remote Desktop Services) を構成しなくても利用できます。

## ログファイル出力

テスト工程などでの不具合確認として、トレースログをファイル出力しておくことは有効な手段となります。
該当機能は、有名どころとして [NuGet Gallery | log4net](https://www.nuget.org/packages/log4net/) で公開されている log4net がありますが、単純機能で設定いらずのクラスを自作して、必要に応じて、対象アプリ向けにカスタマイズして利用していました。
トレースログの実装は、グローバルインスタンスを利用することが手軽なので、下記のようなクラスとしていました。

https://github.com/chaichai0917/SampleCode

- トレースログ ファイル出力
  - TraceLogging.cs （.NET Framework）
  - TraceLogging-Net.cs （.NET - Null許容参照型を明示）

:::note
C# 8.0 で「null安全」が導入されたため、.NET Framework と .NET で別ソースとしています。
従来、string などの参照型は null を許容していましたが、「null安全」が有効な場合、null代入を許容するには string? と Null許容参照型の明示が必要となったためです。

「null安全」は、実行時に null 参照例外（NullReferenceException）が発生しうるコードをコンパイル時にエラーとして認識可能とするために導入されました。
:::

上記のプロジェクトへの追加、アプリ設定、ログファイル出力、TraceLogging クラス説明を以降で記載します。

## プロジェクトへの追加

.NET Fremework と .NET でサンプルソースが異なるので、それぞれについて以降で記載します。

### .NET Framework

前述 TraceLogging.cs をダウンロードして、namespece Hoge となっている部分を、対象プロジェクトの namespece に変更します。

```CSharp:TraceLoggin.cs
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;

namespace Hoge    // ←　ここを変更
```

変更した TraceLogging.cs を対象プロジェクトのフォルダに複写して、ソリューションエクスプローラ `追加` - `既存の項目` で追加します。

### .NET

前述 TraceLogging-Net.cs をダウンロードして、TraceLogging.cs にリネームします。
次に namespece Hoge となっている部分を、対象プロジェクトの namespece に変更します。

```CSharp:TraceLoggin.cs
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;

namespace Hoge    // ←　ここを変更
```

変更した TraceLogging.cs を対象プロジェクトのフォルダに複写して、ソリューションエクスプローラ `追加` - `既存の項目` で追加します。

## アプリ設定

メインフォーム（メインウィンドウ）実行の前後で、トレースログの前処理/後処理を呼び出します。
Windows Forms / WPF、.NET Framework / .NET ともに同一記述ですが、記述箇所とその周辺に差異があるので、それぞれについて以降で記載します。

### Windows Forms - .NET Framework

```CSharp:Program.cs
internal static class Program
{
  [STAThread]
  static void Main()
  {
    // トレースログ - 前処理
    TraceLogging.PreProcessing(); 

    // メインフォーム
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.Run(new Form1());

    // トレースログ - 後処理
    TraceLogging.PostProcessing();
  }
}
```

### Windows Forms - .NET

```CSharp:Program.cs
internal static class Program
{
  [STAThread]
  static void Main()
  {
    // トレースログ - 前処理
    TraceLogging.PreProcessing(); 

    // メインフォーム
    ApplicationConfiguration.Initialize();
    Application.Run(new Form1());

    // トレースログ - 後処理
    TraceLogging.PostProcessing();
  }
}
```

### WPF - .NET Framework / .NET

.NET Framework / .NET ともに同一ソースコードです。
[WPF定石 - スタートアップルーチン](https://qiita.com/chai0917/items/1d1ca54ca82ab2145094) 記載内容を実施済みのコードをベースとします。

```CSharp:App.xaml.cs
public partial class App : Application
{
  private void App_Startup(object sender, StartupEventArgs e)
  {
    // トレースログ - 前処理
    TraceLogging.PreProcessing(); 

    // メインウィンドウ
    var mainWindow = new MainWindow();
    mainWindow.Show();
  }
  private void App_Exit(object sender, ExitEventArgs e)
  {
    // トレースログ - 後処理
    TraceLogging.PostProcessing();
  }
}
```

## ログファイル出力

グローバルインスタンスとしているので、同一 namespace のコードから下記記述でログファイル出力が可能です。

```CSharp
TraceLogging.LogObj?.WriteLine("Hoge Hoge");
```

:::note
上記コードの `?` は、C# 6.0 で導入された  Null条件演算子です。
「`?` の左側にあるオブジェクト（上記例では LogObj）が nullでなければ該当するメンバーを返し、nullならばnullを返す（以降の処理は行わない）」というもので、以下のコードと等価です。

if (TraceLogging.LogObj != null)
{
　TraceLogging.LogObj.WriteLine("Hoge Hoge");
}
:::


ファイル出力は、バッファリングされるので、適時 DataFlush を呼び出すことで、バッファリングされている情報を強制的にファイル出力させることができます。

```CSharp
TraceLogging.LogObj?.WriteLine("Hoge Hoge");
TraceLogging.LogObj?.DataFlush();
```

## TraceLogging クラス説明

.NET Framework 向けのソースをベースとして、いくつかのポイントについて説明します。

### グローバルインスタンス化

グローバルインスタンスとするために public static class としています。

```CSharp:TraceLogging.cs
public static class TraceLogging
{
  // TODO
}
```

### ログファイル作成フォルダ

RDP 運用時、GetTempPath は `C:\Users\[username]\AppData\Local\Temp\2` のように序数サブフォルダを付与したパスが取得されます。
これは、同一アカウントで複数 RDP 接続した場合を考慮して、ワークフォルダをそれぞれ別フォルダとするためです。
このように序数サブフォルダが付与されたフォルダは、RDPセッション終了で削除されてしまいます。
このため、GetFolderPath を用いて  `C:\Users\[username]\AppData\Local\Temp` を取得するようにしています。

```CSharp:TraceLogging.cs - OpenWorkFile
string tmpdir = JoinFilePath(
    Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), 
    "Temp");
string workpath = JoinFilePath(tmpdir, filename);
```

GetFolderPath の引数は下記を参照してください。

https://learn.microsoft.com/ja-jp/dotnet/api/system.environment.specialfolder


JoinFilePath は、本クラス内のメソッドで、フォルダとして指定されたパス末尾にバス区切り文字があったら削除してから、ファイル名と結合をしています。

### ログファイル名

前述ログファイル作成フォルダを RDP 運用時でも序数サブフォルダ無しとしたので、ログファイル名に端末名を付与して、同一アカウントでの RDP 複数接続時に別ファイルとなるようにしています。

```CSharp:TraceLogging.cs - TraceLogObject
string progname = Process.GetCurrentProcess().ProcessName;
string terminal = GetTerminalName();

// RDP運用を考慮して端末名を付与
string filename = string.Format("{0}-{1}.log", progname, terminal);
```

progname は、該当アプリ名称です。
GetTerminalName は、本クラス内のメソッドで、RDP運用時は RDP 接続元の端末名、RDP運用でない場合は自端末名を取得します。

:::note
同一セッションで、同一アプリを複数同時起動するケースでは、ファイル名に pid などを付与して、独立したファイルとしてください。
pid を付与すると、作成されるファイルが増殖してしまうので、本サンプルでは付与していません。

pid は、Process.GetCurrentProcess().Id で取得できます。
:::

### ログファイル排他オープン

ログファイル出力が複数アプリで競合した場合、ログ出力がぐちゃぐちゃになってしまいます。
このため、ログファイルを排他オープンして、同一ファイルを同時利用できなくしています。
具体的には、まず FileStream で排他オープンした後、StreamWriter として生成しています。

```CSharp:TraceLogging.cs - OpenWorkFile
// 排他オープンのため、まずは FileStream 生成
stream = new FileStream(workpath, FileMode.Create, FileAccess.Write, FileShare.None);
StreamWriter writer = new StreamWriter(stream, encoding);
```

### Exception 握り潰し
「ログ出力機能で、何らかの不足の事態により例外を発生させて、アプリ動作を止めてしまうのは望ましくない」という判断で、TraceLooging.cs 内では、下記記述で Exception を握り潰しています。

```CSharp:TraceLogging.cs - TraceLogObject
try
{
  // TODO
}
catch { /* NOP */ }
```


### 補足

リリースしたソフトでも不具合時の情報収集として、トレースログは有効です。
トレースログの有効/無効を設定ファイルなどによって制御する場合には、前処理 PreProcessing を下記のように書き換えてください。

```CSharp:TraceLogging.cs
public static void PreProcessing()
{
  // ログ出力有効/無効をアプリ設定から取得
  bool bEnable = IsLoggingEnable();   // IsLoggingEnable はそれぞれで実装してください。 
  if (bEnable)
  {
    // トレースログ - 初期化
    LogObj = new TraceLogObject();
  }
  else
  {
    LobObj = null;
  }
}
```