.NET Core の設定情報の仕組みをしっかり理解したい方向け基本のキ

f:id:ecb_mkobayashi:20190924113406p:plain

こんにちは、アーキテクトの小林です。

.NET Framework は長年に渡って多くの Windows ベースのアプリケーションの開発現場で採用されてきたものですから、.NET Core への移行は「まだまだこれから」という状態ではないかと思います。

.NET Framework のアプリケーションを .NET Core に移行しようと思ったとき、違いが大きすぎて最初に困惑するポイントが設定情報の管理方法の違いであろうかと思っています。

当社の主力製品である ecbeing も .NET Framework でつくられていますが、ecbeing は EC のパッケージ製品ですのでお客様のニーズに合わせて設定による機能のオンオフや、機能の挙動の変更が可能になっており、非常に多くの設定項目が存在します。

したがって設定情報の管理の柔軟さは ecbeing 社のエンジニアにとっては、とてもとても重要な興味・関心事となっています。ということで、今回は .NET Core の設定情報の仕組みについて自分の理解している内容の整理も兼ねて記事としてまとめてみることにしました。

従来の .NET Framework の設定情報の管理方法

まずは従来の .NET Framework における設定情報の一般的な管理方法について振り返ってみましょう。 多くは、App.config(または Web.config) の <appSettings> に記述するか、独自にXMLの構造を読み取るクラスを用意していたのではないかと思います。

App.config の appSettingsに記述する方法

非常にポピュラーで極めてお手軽な管理方法です。ConfigurationManager クラスを使って読みだしていました。

app.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="MY_SETTING_KEY" value="false" />
  </appSettings>
</configuration>

Program.cs

using System;
using System.Configuration;
namespace ConsoleFramework
{
    class Program
    {
        static void Main(string[] args)
        {
            if (MyUtil.GetSetting("MY_SETTING_KEY"))
            {
                Console.WriteLine("enabled");
            }
            else
            {
                Console.WriteLine("disabled");
            }
           
            Console.ReadLine();
        }
    }
    public static class MyUtil
    {
        public static bool GetSetting(string key)
        {
            return bool.Parse(ConfigurationManager.AppSettings[key]);
        }
    }
}

App.config に独自のXML構造で記述する方法

構造化された設定情報を扱いたい場合の管理方法です。独自のXML構造を読み取るクラスを定義して、冒頭の <configSections> で宣言する必要がありました。

app.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="myCustomSection" type="ConsoleFramework.MySectionHandler, ConsoleFramework" />
  </configSections>
  <myCustomSection>
    <mySettingKey value="true" />
  </myCustomSection>
</configuration>

Program.cs

using System;
using System.Configuration;
namespace ConsoleFramework
{
    class Program
    {
        static void Main(string[] args)
        {
            var settingSection = (MySectionHandler)ConfigurationManager.GetSection("myCustomSection");
            if (settingSection.MySettingKey.Value)
            {
                Console.WriteLine("enabled");
            }
            else
            {
                Console.WriteLine("disabled");
            }
           
            Console.ReadLine();
        }
    }
    public sealed class MySectionHandler : ConfigurationSection
    {
        [ConfigurationProperty("mySettingKey")]
        public ChildConfigElement MySettingKey
        {
            get { return (ChildConfigElement)this["mySettingKey"]; }
            set { this["mySettingKey"] = value; }
        }
    }
    public sealed class ChildConfigElement : ConfigurationElement
    {
        [ConfigurationProperty("value")]
        public bool Value
        {
            get { return bool.Parse(this["value"].ToString()); }
            set { this["value"] = value; }
        }
    }
}

従来の方法の問題点

小規模なほとんどのアプリケーションはこのような設定情報の管理で十分ではありましたが、この設定情報に対して以下のような要件が出てきたらどうでしょうか?

  • テスト環境は別の設定を使いたい
  • 環境設定の値で上書きできるようにしたい
  • アプリケーション実行時のパラメータで引数で渡せるようにしたい
  • 機密情報なので設定ファイルに生で書きたくない
  • データベースに設定情報を保持したい

最初の「テスト環境は別の設定を使いたい」という要件については一応 Transform という方法で実現可能ではありますが、独特な構文を覚える必要があってあまりお手軽感がありません。
その他のいろいろな要件にも対処しようとすると .NET Framework の標準的な設定管理方法では機能的に不十分で、ややこしい処理を自分で実装する必要がありました。独自のXML構造で設定を管理していた場合は「あきらめる」という選択肢が現実的と思えるほど手間がかかりそうです。

新しい ASP.NET Core の設定情報の管理方法

ASP.NET Core の設定情報は Options Pattern によって一元管理されるようになっています。
ただ、上記の記事をいきなり読んでもチンプンカンプンですぐに理解にたどり着くのは難しいです。 まずは Visual Studio 2019 Community をインストールして、ASP.NET Core Web アプリケーションを作成して以下の Startup クラスのコンストラクタにブレークポイントを置いて実行してみましょう。

f:id:ecb_mkobayashi:20200313152903p:plain

configuration.Providers というプロパティに5個のプロバイダが登録されていることが確認できます。 これは、ASP.NET Core の Program クラスの CreateDefaultBuilder メソッドの呼び出しによって自動的にお膳立てされた設定情報のプロバイダです。(何を言っているのだかさっぱりわからないかと思いますが、ここは一旦読み進めてください)

Program.cs

namespace ASP.NET
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }
        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)   // ★★★ココです★★★
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}

CreateDefaultBuilder メソッドは ASP.NET Core のフレームワーク内のメソッドですのでブラックボックス化されており、ソースコードを見ることができません。ただ、.NET Core はオープンソースですので逆アセンブルする必要はありません。ブラウザで .NET Core Source Browser にアクセスしてメソッド名で検索してみましょう。

f:id:ecb_mkobayashi:20200313153814p:plain

いくつか検索に引っかかりましたが、オーバーロードがたくさん出てきました。この中の Microsoft.AspNetCore の上から二番目が探している CreateDefaultBuilder メソッドです。そのメソッド内のこの記事に関連する部分の処理が以下です。

builder.ConfigureAppConfiguration((hostingContext, config) =>
{
    var env = hostingContext.HostingEnvironment;

    config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
          .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);

    if (env.IsDevelopment())
    {
        var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
        if (appAssembly != null)
        {
            config.AddUserSecrets(appAssembly, optional: true);
        }
    }

    config.AddEnvironmentVariables();

    if (args != null)
    {
        config.AddCommandLine(args);
    }
})

ここで config インスタンスに対して呼び出しているメソッドが、JSONファイルの設定情報や環境変数、コマンドラインといったさまざまなデータソースから設定情報を読み取るためのプロバイダを設定しています。

ここで各種メソッドを軽く解説しておきます。

AddJsonFile ... JSONファイルで記述された設定情報を読み取ります
AddUserSecrets ... ユーザーシークレットというWindowsにログオンしているユーザー専用の特殊なJSONファイルに記述された設定情報を読み取ります
AddEnvironmentVariables ... 環境設定に定義された設定情報を読み取ります
AddCommandLine ... コマンドラインパラメータで指定した設定情報を読み取ります

それぞれのメソッド内の詳細をさておき、重要なことは共通してフラットな KeyValue データ構造に変換しているということです。

フラットなKeyValue データ構造に変換される設定情報

もう少し掘り下げましょう。
最初の AddJsonFile の引数にはASP.NET Core Web アプリケーションで最初から用意されている appsettings.json が渡されています。そのファイルは以下のようなJSONファイルとなっています。

appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

AddJsonFile メソッド内ではこれを次のような KeyValue データ構造に変換しています。

Key Value
Logging:LogLevel:Default Information
Logging:LogLevel:Microsoft Warning
Logging:LogLevel:Microsoft.Hosting.Lifetime Information
AllowedHosts *

構造化が :(コロン) 区切りのフラットな KeyValue になっています。 ためしに、先ほどのブレークポイントでイミディエイトウィンドウをつかって configuration に configuration["Logging:LogLevel:Default"] でアクセスしてみましょう。

f:id:ecb_mkobayashi:20200313154749p:plain

Information という値の応答がありましたね。 ここまででフラットな KeyValue データ構造に変換という意味の理解ができたのではないかと思います。

後勝ち方式でカスケーディングされる設定情報

Startupクラスのコンストラクタの引数で渡ってくるこの IConfiguration インターフェースを実装した configuration インスタンスは、さまざまな ConfigurationProvider から提供される情報をフラットにした KeyValue データの構造体となっています。

そして、KeyValue データ構造はプロバイダーを記述した順番によって後勝ちでカスケーディングされるようになっています。

CreateDefaultBuilder メソッドで追加されていた各種 Add メソッドの記述順番は以下のようになっています。

[先]

AddJsonFile [appsettings.json]
AddJsonFile [appsettings.Development.json]
AddUserSecrets
AddEnvironmentVariables
AddCommandLine

[後]

この順番で記述していますので、先に呼び出しているJSONファイルで記述した設定情報は、環境変数やコマンドラインパラメータで上書きができるということです。

なお、環境設定については、コロンが使えない環境があるということでコロンをアンダーバー二つにして次のように記述します。

Logging__LogLevel__Default
↑
Logging:LogLevel:Default の意味

それでは実際に VisualStudio のデバッグ実行時の環境変数にLogging__LogLevel__DefaultWarningと設定してみて、後勝ちで設定が上書きされているか確認してみましょう。

f:id:ecb_mkobayashi:20200313160204p:plain

先ほどのブレークポイントで確認してみましょう。

f:id:ecb_mkobayashi:20200313160404p:plain

うまくいっているみたいです。

なお、デフォルトの CreateDefaultBuilder メソッドでは呼び出されていませんが、他にも用意されているプロバイダとして、INIファイル、XMLファイル、インメモリなど多様なプロバイダが最初から用意されています。 もちろん自分で作成した独自クラスも組み込み可能ですので、たとえばデータベースに保存されている情報もこの仕組みの中に入れることができるようになっています。

ASP.NET Core に 従来の .NET Framework の App.config を読み込ませる

さて、最後に過去の XML 設定資産の再利用ということで、冒頭でご紹介した .NET Framework の App.config を ASP.NET Core に組み込んでみましょう!

使うのは「独自の XML 構造で記述する方法」としてご紹介したこちらのファイル。これをプロジェクトフォルダの直下に追加しましょう。

app.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <configSections>
    <section name="myCustomSection" type="ConsoleFramework.MySectionHandler, ConsoleFramework" />
  </configSections>
  <myCustomSection>
    <mySettingKey value="true" />
  </myCustomSection>
</configuration>

f:id:ecb_mkobayashi:20200314154146p:plain
つづいて Program クラスの CreateDefaultBuilder メソッドの呼び出し直後に以下のように ConfigureAppConfiguration メソッドを追加しましょう。

Program.cs

namespace ASP.NET
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureAppConfiguration(config =>  // ★★ココから★★
                {
                    config.AddXmlFile("app.config");
                })                                    // ★★ココまで★★
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}

Startup クラスのコンストラクタのブレークポイントで様子を見てみましょう。

f:id:ecb_mkobayashi:20200314154801p:plain

configuration.Providers プロパティに6個目のプロバイダ XmlConfigurationProvider が追加されていて、XML の内容がフラットな KeyValue データ構造に変換されていることが確認できます。わざわざ独自に XML の構造を読み取るクラスを用意すること無く、configuration["myCustomSection:mySettingKey:value"]の呼び出しによって設定の値を取得できましたね。

本当にわずかなコーディングを追加するだけで、簡単に XML の設定情報を組み込むことができてしまいました。

まとめ

今回は、ASP.NET Core ではさまざまなプロバイダから提供される情報をフラットな KeyValue データ構造に変換し、後勝ち方式でカスケーディングされるということをご紹介しました。

なお、.NET Core 3.0 からは ASP.NET Core ではない .NET Core のコンソールアプリケーションでも、このような設定管理の機構を簡単に組み込めるようになりました。

次回は、これらの KeyValue データ構造を独自クラスのプロパティにバインドしたり、ファイルなどのデータソースが更新されたときに自動的に設定情報が再読み込みされるという IOptionsMonitorという仕組みについて記事にしていきたいと思います。

~ecbeingでは、フレームワークのコアまで覗いてメカニズムを理解するような、ディープなエンジニアも大募集中です!~ careers.ecbeing.tech