WPF アプリケーションを .NET 9 に移行・新規開発するなら、ログ基盤は最初にきちんと設計しておきたいところです。本記事では Microsoft.Extensions.Logging と依存性注入(DI)を使い、ILogger<T> を ViewModel や Window にスマートに注入する最新パターンを、WPF 向けにフルセットで解説します。
.NET 9 の WPF で Microsoft.Extensions.Logging を使う全体像
.NET 9 では、Microsoft.Extensions.* 系のライブラリとして「依存性注入(DI)」「設定(Configuration)」「ロギング」が一体で提供されています。これらは ASP.NET Core や Worker サービスと同じスタックで、ILogger API による高性能かつ構造化されたログ出力が利用できます。
WPF アプリでも同じ考え方で構成できます。ポイントは次のとおりです。
- App.xaml.cs で
IServiceCollection(DI コンテナ)を組み立てる services.AddLogging(...)で ILogger を登録する- Window や ViewModel を DI コンテナから生成し、コンストラクターで
ILogger<T>を受け取る - 必要に応じて .NET Generic Host(
Microsoft.Extensions.Hosting)を使うと、設定ファイルとの連携やライフサイクル管理が楽になる
前提環境とプロジェクト設定
TargetFramework(net9.0-windows)の設定
WPF のような Windows 専用 UI フレームワークは、OS 固有 API を含む OS 指定付き TFM(Target Framework Moniker)を使うのが推奨です。.NET 9 の場合は net9.0-windows を指定します。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
</PropertyGroup>
</Project>
Visual Studio 2022 で WPF (.NET 9) プロジェクトを作成した場合も、念のため .csproj を開いて TFM を確認しておくと安心です。
NuGet パッケージの追加 (.NET 9 / WPF)
WPF テンプレートにはロギングが自動では含まれていないため、必要なパッケージを明示的に追加します。.NET 9 向けには 9.x 系の Microsoft.Extensions.* パッケージが公開されています。
dotnet add package Microsoft.Extensions.Logging
dotnet add package Microsoft.Extensions.Logging.Console
dotnet add package Microsoft.Extensions.Logging.Debug
dotnet add package Microsoft.Extensions.DependencyInjection
# optional: Generic Host / 設定ファイル連携を使う場合
dotnet add package Microsoft.Extensions.Hosting
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.Logging.Configuration
主なパッケージの役割をまとめると次のようになります。
| パッケージ名 | 役割 | 主な用途 |
|---|---|---|
Microsoft.Extensions.Logging | ILogger インターフェイスとロギング API の本体 | ログ基盤のコア。ほぼ必須 |
Microsoft.Extensions.Logging.Console | コンソール出力プロバイダー | 開発中にターミナルにログを表示 |
Microsoft.Extensions.Logging.Debug | Debug 出力プロバイダー | Visual Studio の [出力] ウィンドウ向けログ |
Microsoft.Extensions.DependencyInjection | 組み込み DI コンテナ | ViewModel やサービスの生成とライフサイクル管理 |
Microsoft.Extensions.Hosting | .NET Generic Host 実装 | ASP.NET Core と同じパターンで WPF を構成したい場合 |
Microsoft.Extensions.Configuration.* | appsettings.json を読み込む設定機構 | ログレベルなどを JSON で外出しする場合 |
.NET 9 は標準サポート(STS)であり、依存パッケージも頻繁に更新されるため、ときどき dotnet list package --outdated でバージョンを確認しておくとよいでしょう。
App.xaml / App.xaml.cs で DI コンテナとロガーを構成する(シンプル版)
App.xaml から StartupUri を外す
App.xaml に StartupUri="MainWindow.xaml" が残っていると、DI を通さずに MainWindow が生成されてしまうため、DI を導入する場合は削除します。
<Application x:Class="WpfLoggingSample.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Application.Resources>
</Application.Resources>
</Application>
ServiceCollection を使った最小構成
まずは .NET Generic Host を使わない、最小限の構成から見ていきます。.NET の DI は IServiceCollection にサービスを追加し、ServiceProvider を構築するスタイルが基本です。
using System;
using System.Windows;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace WpfLoggingSample;
public partial class App : Application
{
public IServiceProvider Services { get; private set; } = null!;
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var services = new ServiceCollection();
ConfigureServices(services);
Services = services.BuildServiceProvider();
// MainWindow を DI コンテナから取得して起動
var mainWindow = Services.GetRequiredService<MainWindow>();
mainWindow.Show();
}
private static void ConfigureServices(IServiceCollection services)
{
// ロギングの登録
services.AddLogging(logging =>
{
logging.ClearProviders(); // 既定のプロバイダーをクリア
logging.AddConsole(); // コンソール
logging.AddDebug(); // VS の出力ウィンドウ
logging.SetMinimumLevel(LogLevel.Information);
});
// アプリ固有のサービス・ViewModel・Window
services.AddTransient<MainViewModel>();
services.AddTransient<MainWindow>();
}
}
このパターンでは、コンソール(AddConsole)と Debug(AddDebug)の 2 つのプロバイダーを登録しています。Debug プロバイダーは Visual Studio の「出力」ウィンドウ(デバッグ > 出力)にログを書き出すため、WPF のようにコンソールウィンドウが出ないアプリと相性が良いです。
Services プロパティの使い方
上記の例では App.Services に DI コンテナを保持しています。例えば XAML から生成されるコントロールでサービスを解決したいときは、
var services = ((App)Application.Current).Services;
var logger = services.GetRequiredService<ILogger<SomeControl>>();
といった形で取り出せます。ただし、サービスロケーター的な使い方は乱用するとテストしにくくなるため、基本はコンストラクターインジェクションで完結させるのをおすすめします。
Generic Host を使った構成(発展編)
ASP.NET Core や Worker サービスと同じように、WPF アプリを .NET Generic Host 上で動かすこともできます。Generic Host はアプリのライフサイクルと DI・設定・ロギングを一元管理する仕組みで、HostApplicationBuilder を使って構成します。
WPF ではおおよそ次のような構成になります。
using System;
using System.Threading.Tasks;
using System.Windows;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace WpfLoggingSample;
public partial class App : Application
{
private IHost? _host;
public static IServiceProvider Services
=> ((App)Current)._host?.Services
?? throw new InvalidOperationException("Host is not initialized.");
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var builder = Host.CreateApplicationBuilder();
// ロギングや他サービスの登録
ConfigureServices(builder.Services);
// 必要なら builder.Logging でロギング設定をカスタマイズ
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.AddDebug();
builder.Logging.SetMinimumLevel(LogLevel.Information);
_host = builder.Build();
var mainWindow = Services.GetRequiredService<MainWindow>();
mainWindow.Show();
}
protected override async void OnExit(ExitEventArgs e)
{
if (_host is not null)
{
await _host.StopAsync();
_host.Dispose();
}
base.OnExit(e);
}
private static void ConfigureServices(IServiceCollection services)
{
services.AddTransient<MainViewModel>();
services.AddTransient<MainWindow>();
}
}
Host.CreateApplicationBuilder() を使うと、既定で appsettings.json などの設定ファイルと統合されたロギング構成が有効になり、Console / Debug / EventSource / EventLog といったプロバイダーが自動で登録されます。 そのため、ASP.NET Core とほぼ同じ感覚でログレベルや出力先を切り替えられるのが利点です。
MainViewModel / Window に ILogger<T> を注入する
MainViewModel の例
典型的な MVVM 構成では、ViewModel に ILogger を注入して画面単位のイベントを記録します。ILogger はスレッドセーフであり、UI スレッド・バックグラウンドスレッドのどちらから呼んでも問題ありません。
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.Logging;
namespace WpfLoggingSample;
public class MainViewModel : INotifyPropertyChanged
{
private readonly ILogger<MainViewModel> _logger;
public MainViewModel(ILogger<MainViewModel> logger)
{
_logger = logger;
_logger.LogInformation("MainViewModel created at {Time}", DateTimeOffset.Now);
}
private string _userName = "ゲスト";
public string UserName
{
get => _userName;
set
{
if (_userName == value) return;
_userName = value;
OnPropertyChanged();
_logger.LogDebug("UserName changed to {UserName}", value);
}
}
public void Save()
{
try
{
_logger.LogInformation("Save command started.");
// 何らかの保存処理...
_logger.LogInformation("Save command completed.");
}
catch (Exception ex)
{
_logger.LogError(ex, "保存処理中にエラーが発生しました。");
throw;
}
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
MainWindow への注入と DataContext 設定
Window 側でも ILogger を受け取り、起動時の情報を記録できます。ViewModel は DI コンテナから供給し、そのまま DataContext に設定します。
using System.Windows;
using Microsoft.Extensions.Logging;
namespace WpfLoggingSample;
public partial class MainWindow : Window
{
private readonly ILogger<MainWindow> _logger;
public MainWindow(MainViewModel viewModel, ILogger<MainWindow> logger)
{
InitializeComponent();
_logger = logger;
DataContext = viewModel;
_logger.LogInformation("MainWindow initialized.");
}
private void OnClosed(object? sender, System.EventArgs e)
{
_logger.LogInformation("MainWindow closed.");
}
}
これだけで、MainViewModel / MainWindow それぞれのカテゴリ名(型名)でログが発行されるようになります。カテゴリごとのログレベルは、後述する appsettings.json で細かく制御できます。
ログレベルとプロバイダーの設計
ログレベルの整理
Microsoft.Extensions.Logging のログレベルは次の 7 段階です。
| レベル | 用途の目安 | 本番環境での推奨 |
|---|---|---|
| Trace | 詳細すぎる内部状態。性能計測や一時的な調査向け | 通常は無効 |
| Debug | 開発・デバッグ専用の詳細ログ | 必要なクラスだけ有効にする |
| Information | ユーザー操作や正常系のイベント(画面遷移・保存成功など) | デフォルトとして最も無難 |
| Warning | リトライで回復したエラー・怪しい状態 | 常に記録 |
| Error | 機能の失敗。例外発生など | 常に記録 |
| Critical | アプリ全体に影響する致命的な障害 | 常に記録。アラートのトリガーに |
| None | ログを無効化するための特別な値 | 特定カテゴリを完全に止めたい場合のみ |
WPF アプリの場合、「ユーザー操作の流れ(画面表示・ボタン押下・保存完了)」には Information、「開発中だけ追いたいバインディングの変化」には Debug といったように、用途で使い分けると後からログを読みやすくなります。
代表的なログプロバイダー
.NET のロギングは、ILogger で書かれたログを「プロバイダー」と呼ばれるコンポーネントが各出力先に転送します。組み込みでは Console / Debug / EventSource / EventLog などが提供されており、必要なものだけを追加できます。
| プロバイダー | 追加メソッド | 主な用途 |
|---|---|---|
| Console | logging.AddConsole() | ターミナル/コンソール出力。単体テストや CI ログにも |
| Debug | logging.AddDebug() | Visual Studio の [出力] ウィンドウ。WPF ではほぼ必須 |
| EventLog (Windows) | logging.AddEventLog() | Windows イベントログへの書き込み。運用監視向け |
| EventSource | logging.AddEventSourceLogger() | ETW / PerfView などによる詳細診断 |
| ファイル系(サードパーティ) | logging.AddFile(...) など | テキストファイルへのローテーションログ |
ファイル出力は Serilog や NLog など、Microsoft.Extensions.Logging と連携できるロガーを使うのが一般的です。これらは AddSerilog() や AddNLog() といった拡張メソッドで簡単に統合できます。
appsettings.json でログ設定を外出しする
ログレベルや出力先をコードにベタ書きしてしまうと、本番環境でちょっとログレベルを上げたいだけの変更でも再ビルドが必要になります。そこで、ASP.NET Core と同じく appsettings.json に Logging セクションを定義し、設定で制御するのが .NET 9 の主流です。
appsettings.json の例
{
"Logging": {
"LogLevel": {
"Default": "Information",
"WpfLoggingSample": "Debug",
"Microsoft": "Warning"
},
"Console": {
"IncludeScopes": true
}
}
}
上記では、アプリケーション自体(名前空間 WpfLoggingSample 以下)は Debug 以上、Microsoft.* 系のログは Warning 以上だけを出力するように調整しています。
Generic Host を使う場合の連携
Generic Host(Host.CreateApplicationBuilder)を利用している場合、appsettings.json は既定で読み込まれ、Logging セクションも自動で反映されます。このとき DOTNET_ENVIRONMENT などの環境変数に応じて appsettings.Development.json などもマージされるため、環境ごとのログレベル切り替えも簡単です。
ServiceCollection だけを使う場合の連携
Generic Host を使わずに ServiceCollection だけで構成している場合は、自分で ConfigurationBuilder を使って appsettings.json を読み込み、AddConfiguration でロギングに適用します。
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
// 例: App.ConfigureServices 内
private static void ConfigureServices(IServiceCollection services)
{
var configuration = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.Build();
services.AddSingleton<IConfiguration>(configuration);
services.AddLogging(logging =>
{
logging.ClearProviders();
// appsettings.json の "Logging" セクションを適用
logging.AddConfiguration(configuration.GetSection("Logging"));
logging.AddConsole();
logging.AddDebug();
});
services.AddTransient<MainViewModel>();
services.AddTransient<MainWindow>();
}
reloadOnChange: true としておけば、appsettings.json を編集した際にアプリ起動中でも一部設定が反映されます(ただし、すべてのプロバイダーが動的変更に対応しているわけではない点には注意してください)。
WPF 特有のロギング・ベストプラクティス
DispatcherUnhandledException などのグローバルエラー処理
WPF では UI スレッドで処理されなかった例外を Application.DispatcherUnhandledException で捕捉できます。ここで ILogger を使って記録しておくと、画面単位では拾いきれない予期せぬ例外も漏れにくくなります。
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var services = new ServiceCollection();
ConfigureServices(services);
Services = services.BuildServiceProvider();
var logger = Services.GetRequiredService<ILogger<App>>();
this.DispatcherUnhandledException += (_, args) =>
{
logger.LogError(args.Exception, "未処理の UI 例外が発生しました。");
args.Handled = true; // 必要に応じてアプリ継続可否を検討
};
AppDomain.CurrentDomain.UnhandledException += (_, args) =>
{
if (args.ExceptionObject is Exception ex)
{
logger.LogCritical(ex, "未処理のドメイン例外が発生しました。");
}
};
var mainWindow = Services.GetRequiredService<MainWindow>();
mainWindow.Show();
}
ログの粒度とパフォーマンス
バインディングの変更通知やマウス移動など、高頻度で発生するイベントをすべてログに書き出すと、UI がカクつく原因になります。こうした箇所には Debug レベル以下を使い、本番環境では切るように設計しておくのが安全です。
.NET 9 では LoggerMessageAttribute によるソースジェネレーター型の高性能ロギングも利用でき、文字列連結やボックス化を最小限に抑えたログ出力が可能です。大量ログが予想されるバックグラウンド処理などでは、こうした仕組みを検討するとよいでしょう。
構造化ログで検索しやすくする
ILogger は「メッセージ + 名前付きパラメーター」を持つ構造化ログに対応しています。単なる文字列連結ではなく、次のように書くことでログ集約ツールでの検索性が向上します。
_logger.LogInformation("ユーザー {UserId} がファイル {FilePath} を開きました。",
userId, filePath);
// NG 例: 検索しづらい
_logger.LogInformation($"ユーザー {userId} がファイル {filePath} を開きました。");
後から「特定のユーザー ID だけを抽出」「特定のファイルパスを含むログだけを検索」といった分析を行う可能性があるなら、パラメーター化された構造化ログを意識しておくと役立ちます。
サンプル全体像のまとめ
ここまでの内容を最小限の WPF アプリにまとめると、構成は次のようになります。
- プロジェクト:
net9.0-windowsをターゲットにした WPF アプリ - NuGet: Microsoft.Extensions.Logging / Console / Debug / DependencyInjection などを追加
- App.xaml.cs: ServiceCollection or Generic Host で DI コンテナ + ロギングを構成
- MainViewModel:
ILogger<MainViewModel>をコンストラクターインジェクション - MainWindow:
ILogger<MainWindow>と MainViewModel を注入し、DataContext に設定 - 任意: appsettings.json でログレベル・出力先を切り替え
ASP.NET Core で慣れたロギング&DI の世界を、そのまま WPF (.NET 9) に持ち込めば、UI でもサーバーサイドでも同じ思想でログ基盤を設計できます。最初に基盤を整えておくことで、「障害が起きたけれど何が起きたのかわからない」といった状況を大きく減らせるはずです。

コメント