【C#, WinForms】.NET 5.0 時代の構成ファイル


はじめに

C# デスクトップアプリにて、外部ファイルから構成を読み込みたい

構成:データベースへの接続文字列だとか、何らかの機能の初期値など

(ASP.NETでは、appsettings.jsonを使うらしい)

JSONファイルが登場する以前からある方法として、INIファイルを使う方法がある
(参考:2003年に書かれた atmarkIT の記事

.NET Framework から .NET Core を経て、.NET5(現在) となった今、もっとモダンな方法がないか調べて実際に試してみた

試したときの状況

  • Microsoft Visual Studio Community 2022 (64 ビット) - Preview
    • Version 17.1.0 Preview 3.0
  • Windows フォームアプリ
  • .NET 5.0(現在)

INIファイルを使って構成を読み込む方法

これまで自分が使ってきた方法のおさらい

DLLImport

先述の記事の方法

謎のおまじない[DllImport("KERNEL32.DLL")]が書かれたユーティリティクラスを用意して使う

ini-parser

NuGetからプロジェクトにインストールできる
README.mdを読めばすぐに使える、扱いやすいライブラリ

しかし、2017年が最終リリースのため、.NET Core 以降に対応しておらず・・・

Issue上部に更新が止まった経緯がピン止めされている
コロナ・転職・子ども、などライフイベントで時間がとれなくなったから、メンテナ募集します、とのこと。

互換性がないと表示されるエラーメッセージ

パッケージ 'ini-parser 2.5.2' はプロジェクトのターゲット フレームワーク '.NETStandard,Version=v2.1' ではなく '.NETFramework,Version=v4.6.1, .NETFramework,Version=v4.6.2, .NETFramework,Version=v4.7, .NETFramework,Version=v4.7.1, .NETFramework,Version=v4.7.2, .NETFramework,Version=v4.8' を使用して復元されました。このパッケージは、使用しているプロジェクトとの完全な互換性がない可能性があります。

ini-parser-standard

.NET Standard 2.0 に依存してビルドされたバージョンがある
こちらであれば .NET5 からもサポートされているので、プロジェクトにインストールできる

作者は本家ini-parserと同一で、プロジェクトURLはini-parserと同じだけど
README.mdで言及されていないし、なんかよくわからない代物

参考のIssue

【本題】モダンな方法

MS公式ドキュメントにINI ファイル、JSON ファイル、XML ファイルなどに応じた方法が書かれている

INIファイルについて詳細に書かれているページはこちら

構成ファイルの利点について、わかりやすく書かれている

構成ファイルを利用すると、実行時に読み取られアプリケーションの動作に影響を与える、一連のプロパティを格納できます。たとえば、データベースを配置する場所や、ループを実行する回数などです。 この手法の利点は、コードの書き直しや再コンパイルを行わずに、アプリケーションの一部の側面を変更できることです。

そしてモダンな方法への切り替えを推奨している

.NET の世界に machine.config ファイルはありません。 また、旧式の System.Configuration 名前空間を使用し続けることもできますが、多くの拡張機能を備えたモダンな Microsoft.Extensions.Configuration に切り替えることを検討してください。

モダンな方法を実際に試してみる

実際にプロジェクトを作成し、順を追ってMicrosoft.Extensions.Configurationを使った方法を試す

なお、すでに試されている記事もある(Qiita)(参考にさせていただきました)。

プロジェクトを作成する

.NET 5 フォームアプリのプロジェクトを作成し、必要なパッケージを NuGet からインストールする

パッケージマネージャーコンソール
Install-Package Microsoft.Extensions.Configuration.Ini
Install-Package Microsoft.Extensions.Configuration.Binder
Install-Package Microsoft.Extensions.Logging
パッケージ 用途
Configuration.Ini INIファイルから構成を読みこむため
Configuration.Binder ConfigurationBinder 拡張メソッドを使用するため
Logging 今回あつかう構成ファイル中に"ログレベル"の項目が存在するため

INIファイルを用意する

こちらの公式Docと同じINIファイルを使う
INIファイルを作成したら、exeファイルと同じ階層に配置する
(例)WinFormsApp1\bin\Debug\net5.0-windows\appsettings.ini

appsettings.ini
SecretKey="Secret key value"

[TransientFaultHandlingOptions]
Enabled=True
AutoRetryDelay="00:00:07"

[Logging:LogLevel]
Default=Information
Microsoft=Warning

POCOを作成する

INIファイルの各セクションに対応したオブジェクトを作成する

TransientFaultHandlingOptions.cs
public class TransientFaultHandlingOptions
{
    public bool Enabled { get; set; }
    public TimeSpan AutoRetryDelay { get; set; }
}
Logging
public class Logging
{
    public const string SectionName = "Logging:LogLevel";
    public LogLevel Default { get; set; }
    public LogLevel Microsoft { get; set; }
}

ルートとなるBindingClassクラス

BindingClass
public class BindingClass
{
    public string SecretKey { get; set; }
    public TransientFaultHandlingOptions TransientFaultHandlingOptions { get; set; }
    public Logging Logging { get; set; }
}

INIファイルから構成を読み込む

INIファイルから構成を読み込んで、コンソール出力する

また、変数にバインディングする

Program.cs
using Microsoft.Extensions.Configuration;
using System;
using System.Diagnostics;
using System.Linq;
using System.Windows.Forms;
using Microsoft.Extensions.Logging;

static void Main()
{
    var builder = new ConfigurationBuilder()
        .AddIniFile(path: "appsettings.ini");
    var configuration = builder.Build();

    // コンソール出力する   
    foreach ((string key, string value) in
        configuration.AsEnumerable().Where(t => t.Value is not null))
    {
        Debug.WriteLine($"{key}={value}");
    }

    // 変数にバインディングする
    BindingClass bind = configuration.Get<BindingClass>();
    Logging logging = configuration.GetSection(Logging.SectionName).Get<Logging>();
}

コンソール出力

foreachでキーと値のペアを出力した様子

コンソール出力
TransientFaultHandlingOptions:Enabled=True
TransientFaultHandlingOptions:AutoRetryDelay=00:00:07
SecretKey=Secret key value
Logging:LogLevel:Microsoft=Warning
Logging:LogLevel:Default=Information

変数にバインディング

拡張メソッドをつかってバインディングしたbind変数のLoggingプロパティにはうまくバインディングできていない

Logging変数のDefaultMicrosoftには、デフォルト値のTraceがセットされている

一方でGetSection(Logging.SectionName)により個別で読み込んだlogging変数には、うまくINIファイルの情報が読みこまれている

セクション名が[Logging:LogLevel]のように:で区切られていると、うまくバインディングできない

INIファイルに構成の変更を書きこ・・・めなかった

指定されたキーの構成値を設定するSet(string key, string value);メソッドを見つけはしたものの、うまく保存できない

INIファイルの扱いについて書かれた公式ドキュメントが「保存」に言及していない時点で、書き込むことができない空気は感じていた・・・

GitHub「なぜConfigurationProviderにはセーブ機能がないのか?」

2016年のISSUEより回答を一部引用

  • Configurationは基本的にApp.config(これも読み取り専用)だよ。もし構成を書き換えたいのであれば、データベースとか使ったほうがいいと思うよ

こちら2018年のISSUE より

.NET Core はもはやASP.NETだけのものじゃないのに、保存機能がないのは馬鹿げている・・など書かれている模様

おわりに

いずれにせよAPIの設計思想なんてつよつよエンジニアが決めることだから、私は与えられたもので凌ぐのみ

小さなデスクトップアプリをつくるにしても、なんらかのデータベースを使うことが求められているのかもしれない

(2022.02.26 追記) Settingをつかった構成保存

@Zuishin 様コメントをもとに本項目も追加

参考となる Qiita 記事 ↓↓

参考にさせていただいた記事