【C#】appsetting.jsonをコンソールで扱う

コンソールアプリでも App.congi に代わる新しい定義ファイルの形式である appsettings.json を使用する場合の設定と実装方法の紹介です。

ASP.NET Core および ASP.NET 5~6 であれば、IServiceCollection.Configure にセクション名を渡せば勝手に内容をオブジェクトにマッピングしてくれる機能が存在しますが、コンソールアプリで始めてしまうとそこらへんが存在しないので自分で読み書きする形になります。

したがって、定義ファイルにアクセスしやすいようヘルパークラスの実装例を紹介したいと思います。

確認環境

この記事は以下環境で確認しています。

  • VS2019(16.11.4)
  • .NET 5(C#9.0)
  • コンソールプロジェクトが作成済み

パッケージの導入

「NuGet パッケージの管理」の「参照」から探してもいいですが、微妙にわかりにくかったので「パッケージマネージャー コンソール」からインストールします。

ツール > NuGet パッケージ マネージャー > パッケージ マネージャー コンソール

NuGetのページ: https://www.nuget.org/packages/Microsoft.Extensions.Configuration.Json/

以下コマンドを入力します。

PM>
Install-Package Microsoft.Extensions.Configuration.Json -Version 5.0.0

プロジェクトに間接的に必要なDLLが以下の通り追加されます。

// 結構大量に追加されます
Microsoft.Extensions.Configuration.Abstractions.dll
Microsoft.Extensions.Configuration.dll
Microsoft.Extensions.Configuration.FileExtensions.dll
Microsoft.Extensions.Configuration.Json.dll
Microsoft.Extensions.FileProviders.Abstractions.dll
Microsoft.Extensions.FileProviders.Physical.dll
Microsoft.Extensions.FileSystemGlobbing.dll
Microsoft.Extensions.Primitives.dll

定義ファイルの追加・編集

ソリューションエクスプローラー > 対象プロジェクトのコンテキストメニュー > 追加 > 新しい項目

「JSON ファイル」などの JSON 形式のファイルを選択し「appsetting.json」という名前で追加します。(実際は名前は何でも良いです)追加したらファイルのプロパティでビルドアクションを「コンテンツ」に変更し、出力先ディレクトリにコピーを「新しい場合上書きする」に変更します。

JSON ファイルの内容は以下の通りに編集します。

{
    "MySettings": {
        "Key1": "Key1",
        "Key2": "Key2",
        "InnerSettings": {
            "Key3": "Key3",
            "Key4": "Key4"
        }
    }
}

値の取得

以下のように書けばアクセスできるようになります。

using System.IO;
using Microsoft.Extensions.Configuration;

internal class AppMain
{
    private static void Main(string[] args)
    {
        // 定義ファイルの読み書き用のオブジェクトを初期化する
        IConfigurationRoot configuration = 
            new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                    .AddJsonFile("appsettings.json", true, true).Build();

        IConfigurationSection section = config.GetSection("MySettings");
        string key1 = section["Key1"];
        string key2 = section["Key2"];
        
        // 取得できない場合null、例外は発生しない
        string strN = section["KeyXX"];

        // 入れ子の場合「A:B」という風にコロンを挟む
        IConfigurationSection innerSection = config.GetSection("MySettings:InnerSettings");
        string key3 = innerSection["Key3"];
        string key4 = innerSection["Key4"];
    }
}

アクセス用のヘルパークラスの作成

上記のようなコードを毎回書くのも面倒なのでヘルパークラスを作成して手間を軽減したいと思います。

AppSettingJsonクラス

以前の XML 形式の「アプリケーション 構成ファイル」と似たような形でアクセスできるといいので以下のような「MySettings」セクションの下に key-value 形式で値が並んでいる定義ファイルを想定してヘルパークラスを作成したいと思います。

// appsetting.json
{
    "MySettings": {
        "key1": "stringstring",
        "key2": 10,
        "key3": 123.5698
    }
}

実装は以下の通りです。

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Runtime.CompilerServices;
using Microsoft.Extensions.Configuration;

namespace Takap.Utility
{
    /// <summary>
    /// appsetting.json を簡単に操作できるようにするためのヘルパークラス
    /// </summary>
    public class AppSettingsSection
    {
        #region 見本...
        // 
        // 見本:
        // 以下の通り記述されている appSettingsノードの中身に簡単にアクセスできるようにする 
        //
        // appsetting.json
        // {
        //     "appSettings": {
        //         "key1": "stringstring",
        //         "key2": 10,
        //         "key3": 123.5698
        //     }
        // }
        //
        #endregion

        // 固有の設定ファイルの名前
        private string _filePath;
        // 設定値が格納されているセクションの名前
        private string _section;
        // 読み込んだ設定を保持する
        private IConfiguration _conf;

        //
        // Constructors
        // - - - - - - - - - - - - - - - - - - - -

        public AppSettingsSection(
            string filePath = "appsettings.json", 
            string sectionName = "appSettings")
        {
            var _f = filePath ?? throw new ArgumentNullException(nameof(filePath));
            var _s = sectionName ?? throw new ArgumentNullException(nameof(sectionName));

            _filePath = _f;
            _section = _s;
        }

        //
        // Public Methods
        // - - - - - - - - - - - - - - - - - - - -

        /// <summary>
        /// appSetting セクション内の指定したキーに対応する値を取得します。
        /// </summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public string GetString(string key) => GetValue(key);

        // 以下基本型として値を取得するメソッド
        public bool GetBool(string key) => bool.Parse(GetString(key));
        public byte GetByte(string key) => byte.Parse(GetString(key));
        public sbyte GetSByte(string key) => sbyte.Parse(GetString(key));
        public char GetChar(string key) => char.Parse(GetString(key));
        public decimal GetDecimal(string key) => decimal.Parse(GetString(key));
        public double GetDouble(string key) => double.Parse(GetString(key));
        public float GetFloat(string key) => float.Parse(GetString(key));
        public int GetInt(string key) => int.Parse(GetString(key));
        public uint GetUInt(string key) => uint.Parse(GetString(key));
        public long GetLong(string key) => long.Parse(GetString(key));
        public ulong GetULong(string key) => ulong.Parse(GetString(key));
        public short GetShort(string key) => short.Parse(GetString(key));
        public ushort GetUShort(string key) => ushort.Parse(GetString(key));
        // 時刻で取得
        public DateTime GetDateTime(string key, string format) 
            => DateTime.ParseExact(GetString(key), format, CultureInfo.InvariantCulture);
        // 時間で取得
        public TimeSpan GetTimeSpan(string key, Func<double, TimeSpan> conv)
            => conv(GetDouble(key));

        //
        // Not-Public Methods
        // - - - - - - - - - - - - - - - - - - - -

        // 対象のセクションの値を取得する
        private string GetValue(string key)
        {
            _conf ??= new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                    .AddJsonFile(_filePath, true, true)
                        .Build();

            IConfigurationSection sec =
                _conf.GetSection(_section) ??
                    throw new KeyNotFoundException($"{_section} section is not found.");

            return sec[key] ?? throw new KeyNotFoundException($"Key not found. key={key}");
        }
    }
}

コンストラクタで指定した単一のセクションのみを対象に読み書きを単純化できるような実装にしています。値の取得を GetValur としてもよかったのですが微妙にパフォーマンスが悪かったので各基本型ごとの値の取得メソッドを専用に作成しています。

使い方

使い方ですが以下の通りです。

class Program
{
    static void Main(string[] args)
    {
        // {
        //     "appSettings": {
        //         "key1": "stringstring",
        //         "key2": 10,
        //         "key3": 123.5698
        //     }
        // }
        //
        // 既定だと "appsetting.json" の appSettings を読みに行く
        //
        AppSettingJson setting = new();
        var a = setting.GetString("key1");
        var b = setting.GetInt("key2");
        var c = setting.GetFloat("key3");
        var d = setting.GetFloat("key4"); // KeyNotFoundException


        // {
        //     "customSection": {
        //         "key1111": "stringstring",
        //         "key2222": 10,
        //         "key3333": 123.5698
        //     }
        // }
        //
        // パラメーターを指定すれば任意のセクションを簡単に読める
        //
        AppSettingJson setting2 = new(sectionName: "customSection");
        var a2 = setting2.GetString("key1111");
        var b2 = setting2.GetInt("key2222");
        var c2 = setting2.GetFloat("key3333");
        var d2 = setting2.GetFloat("key4444"); // KeyNotFoundException
        
        
        // フルで指定すれば任意のファイルパスの任意の appSettings 以外のセクションも扱える
        AppSettingJson setting2 = new(@"d:\sample.json", "customSection");
    }
}

以上です。