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

## はじめに

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

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

（ASP.NETでは、appsettings.jsonを使うらしい）

JSONファイルが登場する以前からある方法として、INIファイルを使う方法がある
（参考：[2003年に書かれた atmarkIT の記事](https://atmarkit.itmedia.co.jp/fdotnet/dotnettips/039inifile/inifile.html)）

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

### 試したときの状況

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272511/07e22893-2654-fa47-d400-0ec66f24d249.png)


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

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

### DLLImport

[先述の記事](https://atmarkit.itmedia.co.jp/fdotnet/dotnettips/039inifile/inifile.html)の方法

謎のおまじない`[DllImport("KERNEL32.DLL")]`が書かれた[ユーティリティクラス](https://gist.github.com/heiswayi/56f4707a6cf45161807989db24dc3cea)を用意して使う

### [ini-parser](https://github.com/rickyah/ini-parser)

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

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

:::note info
[Issue上部に更新が止まった経緯がピン止めされている](https://github.com/rickyah/ini-parser/issues/237)
コロナ・転職・子ども、などライフイベントで時間がとれなくなったから、メンテナ募集します、とのこと。
:::

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

> パッケージ '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 からもサポートされている](https://docs.microsoft.com/ja-jp/dotnet/standard/net-standard?tabs=net-standard-2-0#select-net-standard-version)ので、プロジェクトにインストールできる

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272511/6cce995d-3446-14f0-f9c7-1942371108be.png)

[参考のIssue](https://github.com/rickyah/ini-parser/issues/239)

## 【本題】モダンな方法

[MS公式ドキュメント](https://docs.microsoft.com/ja-jp/dotnet/architecture/modernize-desktop/migrate-modern-applications#configuration-on-net)にINI ファイル、JSON ファイル、XML ファイルなどに応じた方法が書かれている

INIファイルについて詳細に書かれているページは[こちら](https://docs.microsoft.com/ja-jp/dotnet/core/extensions/configuration-providers#ini-configuration-provider)

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

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

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

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

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

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

なお、[すでに試されている記事もある(Qiita)](https://qiita.com/c-yan/items/3e0b2503d26693140457)（参考にさせていただきました）。

### プロジェクトを作成する

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

```Powershell:パッケージマネージャーコンソール
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](https://docs.microsoft.com/ja-jp/dotnet/core/extensions/configuration-providers#ini-configuration-provider)と同じINIファイルを使う
INIファイルを作成したら、exeファイルと同じ階層に配置する
（例）WinFormsApp1\bin\Debug\net5.0-windows\appsettings.ini

```ini:appsettings.ini
SecretKey="Secret key value"

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

[Logging:LogLevel]
Default=Information
Microsoft=Warning
```

### POCOを作成する

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

```cs:TransientFaultHandlingOptions.cs
public class TransientFaultHandlingOptions
{
    public bool Enabled { get; set; }
    public TimeSpan AutoRetryDelay { get; set; }
}
```

```cs:Logging
public class Logging
{
    public const string SectionName = "Logging:LogLevel";
    public LogLevel Default { get; set; }
    public LogLevel Microsoft { get; set; }
}
```

ルートとなる`BindingClass`クラス

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

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

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

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

```cs: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`でキーと値のペアを出力した様子

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

#### 変数にバインディング

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

`Logging`変数の`Default`と`Microsoft`には、**デフォルト値の**`Trace`がセットされている

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272511/756d0332-3661-01fb-6107-d35d62515079.png)

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

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

指定されたキーの構成値を設定する`Set(string key, string value);`[メソッド](https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.extensions.configuration.iconfigurationprovider.set?view=dotnet-plat-ext-6.0)を見つけはしたものの、うまく保存できない

[INIファイルの扱いについて書かれた公式ドキュメント](https://docs.microsoft.com/ja-jp/dotnet/core/extensions/configuration-providers#ini-configuration-provider)が「保存」に言及していない時点で、書き込むことができない空気は感じていた・・・

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

[2016年のISSUE](https://github.com/aspnet/Configuration/issues/385)より回答を一部引用

- Configurationは基本的に`App.config`（これも読み取り専用）だよ。もし構成を書き換えたいのであれば、データベースとか使ったほうがいいと思うよ
    - Configuration is basically replacing App.config - which was read-only as well. In my opinion, if you need to write configuration you need some sort of database.
        - ※ そのあとの返信で、`App.config`は書き換え可能と記載あり: https://stackoverflow.com/questions/11149556/app-config-change-value

[こちら2018年のISSUE](https://github.com/dotnet/AspNetApiDocs/issues/89) より

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

## おわりに

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

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

## （2022.02.26 追記） Settingをつかった構成保存

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

参考となる Qiita 記事 ↓↓

https://qiita.com/tera1707/items/2ebc0e5c48dc5226f60c 

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

https://mseeeen.msen.jp/load-settings-to-own-class-from-configuration-in-dotnet-core/

https://docs.microsoft.com/ja-jp/archive/msdn-magazine/2016/february/essential-net-configuration-in-net-core