February 2016

Volume 31 Number 2

Essential .NET - .NET Core における構成

Mark Michaelis

本号発行直前に、マイクロソフトは ASP.NET 5 と関連スタックの名称変更を発表しました。ASP.NET 5 は ASP.NET Core 1.0 に名前が変わります。Entity Framework (EF) 7 は Entity Framework (EF) Core 1.0 に変わります。ASP.NET 5 と EF7 のパッケージと名前空間は変わりますが、それ以外はこの新しい名称が本コラムのテーマに影響することはありません。

Mark MichaelisASP.NET 5 に取り組んでいる開発者であれば、このプラットフォームに新しい構成サポートが追加され、NuGet パッケージの Microsoft.Extensions.Configuration コレクションで利用できるようになっていることにお気づきでしょう。この新しい構成では、名前と値のペアのリストを使用して、構成を複数レベルの階層にグループ化できるようになります。たとえば、SampleApp:Users:Inigo­Montoya:MaximizeMainWindow と、SampleApp:AllUsers:Default:MaximizeMainWindow にそれぞれ別の設定を格納できます。格納した値はすべて、文字列にマッピングされます。また、設定をカスタム POCO オブジェクトにシリアル化解除できる組み込みのバインド サポートがあります。新しい構成 API を既に理解している開発者も、おそらく最初にこれを見たのは ASP.NET 5 でしょう。ですが、この API は、ASP.NET に限定されるものではありません。実際、今回のリストはすべて、Microsoft .NET Framework 4.5.1 を利用する Visual Studio 2015 の単体テスト プロジェクトで、ASP.NET 5 RC1 の Microsoft.Extensions.Configuration パッケージを参照しています (ソース コードは gitHub.com/IntelliTect/Articles (英語) を参照してください)。

構成 API は、インメモリ .NET オブジェクト、INI ファイル、JSON ファイル、XML ファイル、コマンド ライン引数、環境変数、暗号化されたユーザー ストアなどに対する構成プロバイダー、および開発者が作成した任意のカスタム プロバイダーをサポートします。構成に JSON ファイルを利用する場合は、Microsoft.Extensions.Configuration.Json NuGet パッケージを追加するだけです。その後、コマンド ラインを使用して構成情報を入力できるようにする場合は、他の構成の参照に追加で、または他の構成の参照の代わりに、Microsoft.Extensions.Configuration.CommandLine NuGet パッケージを追加します。組み込み構成プロバイダーがどれも不十分であれば、Microsoft.Extensions.Configuration.Abstractions のインターフェイスを実装して、独自の構成プロバイダーを自由に作成できます。

構成設定の取得

構成設定の取得について理解するために、図 1 をご覧ください。

図 1 InMemoryConfigurationProvider 拡張メソッドと ConfigurationBinder 拡張メソッドを使用した構成の基本

public class Program
{
  static public string DefaultConnectionString { get; } =
    @"Server=(localdb)\\mssqllocaldb;Database=SampleData-0B3B0919-C8B3-481C-9833-
    36C21776A565;Trusted_Connection=True;MultipleActiveResultSets=true";
  static IReadOnlyDictionary<string, string> DefaultConfigurationStrings{get;} =
    new Dictionary<string, string>()
    {
      ["Profile:UserName"] = Environment.UserName,
      [$"AppConfiguration:ConnectionString"] = DefaultConnectionString,
      [$"AppConfiguration:MainWindow:Height"] = "400",
      [$"AppConfiguration:MainWindow:Width"] = "600",
      [$"AppConfiguration:MainWindow:Top"] = "0",
      [$"AppConfiguration:MainWindow:Left"] = "0",
    };
  static public IConfiguration Configuration { get; set; }
  public static void Main(string[] args = null)
  {
    ConfigurationBuilder configurationBuilder =
      new ConfigurationBuilder();
      // Add defaultConfigurationStrings
      configurationBuilder.AddInMemoryCollection(
        DefaultConfigurationStrings);
      Configuration = configurationBuilder.Build();
      Console.WriteLine($"Hello {Configuration["Profile:UserName"]}");
      ConsoleWindow consoleWindow =
        Configuration.Get<ConsoleWindow>("AppConfiguration:MainWindow");
      ConsoleWindow.SetConsoleWindow(consoleWindow);
  }
}

構成へのアクセスは、手始めに ConfigurationBuilder のインスタンスを使用するのが簡単です。ConfigurationBuilder は、Microsoft.Extensions.Configuration NuGet パッケージに含まれるクラスです。ConfigurationBuilder インスタンスを用意したら、AddInMemoryCollection のような IConfigurationBuilder 拡張メソッドを使用して、プロバイダーを直接追加します (図 1 参照)。このメソッドは、構成の名前と値のペアとして Dictionary<string,string> インスタンスを受け取り、これを使用して構成プロバイダーを初期化してから、ConifigurationBuilder インスタンスに構成プロバイダーを追加します。構成ビルダーが "構成" されたら、その Build メソッドを呼び出して、構成を取得します。

既に述べたとおり、構成とは、コロンによって区切られたノードの名前と値のペアから成る階層リストです。そのため、特定の値を取得するには、次のように、対応する項目のキーを使用して、Configuration インデクサーにアクセスします。

Console.WriteLine($"Hello {Configuration["Profile:UserName"]}");

ただし、値へのアクセスは文字列を取得するだけではありません。たとえば、ConfigurationBinder の Get<T> 拡張メソッドを使用して値を取得できます。一例として、メイン ウィンドウの画面バッファー サイズを取得するには、次のコードを使用します。

Configuration.Get<int>("AppConfiguration:MainWindow:ScreenBufferSize", 80);

このバインド サポートには、Microsoft.Extensions.Configuration.Binder NuGet パッケージへの参照が必要です。

キーの後にオプションの引数があるのがわかります。この引数に、キーが存在しない場合の既定値を指定できます (既定値を指定しない場合、戻り値には default(T) が割り当てられます。予想とは異なり、例外はスローされません)。

構成値は、スカラーに限りません。POCO オブジェクトや、オブジェクト グラフ全体を取得することもできます。AppConfiguration:MainWindow 構成セクションにマッピングするメンバーを持つ ConsoleWindow のインスタンスを取得するために、図 1 では次のコードを使用しています。

ConsoleWindow consoleWindow =
  Configuration.Get<ConsoleWindow>("AppConfiguration:MainWindow")

また、AppConfiguration などの構成グラフを定義することもできます (図 2 参照)。

図 2 サンプル構成オブジェクト グラフ

class AppConfiguration
{
  public ProfileConfiguration Profile { get; set; }
   public string ConnectionString { get; set; }
  public WindowConfiguration MainWindow { get; set; }
  public class WindowConfiguration
  {
    public int Height { get; set; }
    public int Width { get; set; }
    public int Left { get; set; }
    public int Top { get; set; }
  }
  public class ProfileConfiguration
  {
    public string UserName { get; set; }
  }
}
public static void Main()
{
  // ...
  AppConfiguration appConfiguration =
    Program.Configuration.Get<AppConfiguration>(
      nameof(AppConfiguration));
  // Requires referencing System.Diagnostics.TraceSource in Corefx
  System.Diagnostics.Trace.Assert(
    600 == appConfiguration.MainWindow.Width);
}

このようなオブジェクト グラフを使用して、厳密に型指定されたオブジェクトの階層を持つ構成の全体または一部を定義できます。これを使用して、設定をすべて一度に取得できます。

複数の構成プロバイダー

InMemoryConfigurationProvider は、既定値や (場合によっては) 計算値を格納するのに便利です。しかし、このプロバイダーだけでは、構成を ConfigurationBuilder に登録する前の、構成の取得や Dictionary<string,string> への構成の読み込みに苦労します。さいわい、他にもいくつかの組み込み構成プロバイダーが用意されています。それは、3 つのファイルベース プロバイダー (XmlConfigurationProvider、IniConfigurationProvider、JsonConfigurationProvider)、環境変数プロバイダー (EnvironmentVariableConfigurationProvider)、およびコマンド ライン変数プロバイダー (CommandLineConfigurationProvider) です。さらに、これらのプロバイダーは、アプリケーションのロジックに合わせて、混在させたり、組み合わせたりできます。次の優先順位 (昇順) で構成設定を指定する例を考えてみましょう。

  • InMemoryConfigurationProvider
  • Config.json に対する JsonFileConfigurationProvider
  • Config.Production.json に対する JsonFileConfigurationProvider
  • EnvironmentVariableConfigurationProvider
  • CommandLineConfigurationProvider

要するに、構成の既定値をコードで格納できます。次に、Config.json ファイル、Config.Production.json ファイルの順に InMemory で指定された値がオーバーライドされます。値が重複する場合は、後に指定したプロバイダー (JSON プロバイダーなど) が優先されます。続いて、配置時に、環境変数に格納されたカスタム構成値を取得できます。たとえば、Config.Production.json をハードコーディングする代わりに、Windows の環境変数から環境設定を取得して、環境変数と同一である特定のファイル (おそらくは Config.Test.Json) にアクセスします (%USERNAME%、%USERDOMAIN% などの Windows の環境変数に比べ、運用 (Production)、テスト (Test)、運用前 (Pre-production)、開発 (Development) など、環境設定関連の用語があいまいですみません)。最後に先ほど指定した設定をコマンド ラインから、おそらくは、ログを有効にするなど、1 回のみの変更として、指定 (オーバーライド) します。

各プロバイダーを指定するには、プロバイダーを構成ビルダーに (fluent API の拡張メソッド AddX を使用して) 追加します (図 3 参照)。

図 3 複数の構成プロバイダーの追加 (最後に指定された構成プロバイダーが優先される)

public static void Main(string[] args = null)
{
  ConfigurationBuilder configurationBuilder =
    new ConfigurationBuilder();
  configurationBuilder
    .AddInMemoryCollection(DefaultConfigurationStrings)
    .AddJsonFile("Config.json",
      true) // Bool indicates file is optional
    // "EssentialDotNetConfiguartion" is an optional prefix for all
    // environment configuration keys, but once used,
    // only environment variables with that prefix will be found        
    .AddEnvironmentVariables("EssentialDotNetConfiguration")
    .AddCommandLine(
      args, GetSwitchMappings(DefaultConfigurationStrings));
  Console.WriteLine($"Hello {Configuration["Profile:UserName"]}");
  AppConfiguration appConfiguration =
    Configuration.Get<AppConfiguration>(nameof(AppConfiguration));
}
static public Dictionary<string,string> GetSwitchMappings(
  IReadOnlyDictionary<string, string> configurationStrings)
{
  return configurationStrings.Select(item =>
    new KeyValuePair<string, string>(
      "-" + item.Key.Substring(item.Key.LastIndexOf(':')+1),
      item.Key))
      .ToDictionary(
        item => item.Key, item=>item.Value);
}

JsonConfigurationProvider の場合、ファイルが存在することを必須としても、オプションとしてもかまいません。そのために、AddJsonFile には追加のオプション パラメーターがあります。パラメーターを指定しなければ、ファイルが存在することが必須となり、ファイルが見つからないと System.IO.FileNotFoundException が発生します。JSON の階層的性質を考えると、この構成は構成 API に非常によく適合します (図 4 参照)。

図 4 JsonConfigurationProvider に対する JSON 構成データ

{
  "AppConfiguration": {
    "MainWindow": {
      "Height": "400",
      "Width": "600",
      "Top": "0",
      "Left": "0"
    },
    "ConnectionString":
      "Server=(localdb)\\\\mssqllocaldb;Database=Database-0B3B0919-C8B3-481C-9833-
      36C21776A565;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}

CommandLineConfigurationProvider は、構成ビルダーに登録するときに、引数を指定する必要があります。引数は、名前と値のペアから成る文字列配列で指定します。このペアは、/<name>=<value> の形式で指定し、等号は必須です。先頭のスラッシュも必須ですが、2 つ目のパラメーター AddCommandLine(string[] args, Dictionary<string,string> switchMappings) は必須ではありません。関数を使用して、別名を指定できます。この別名は、先頭に - または -- を付ける必要があります。たとえば、値のディクショナリは、コマンド ラインに「program.exe -LogFile="c:\programdata\Application Data\Program.txt"」と入力して、構成要素 AppConfiguration:LogFile に読み込むことができます。

["-DBConnectionString"]="AppConfiguration:ConnectionString",
  ["-LogFile"]="AppConfiguration:LogFile"

構成の基本を締めくくるにあたって、いくつか追加の注意点があります。

  • CommandLineConfigurationProvider には、IntelliSense で直感的に理解できない、注意が必要ないくつかの特徴があります。
    • CommandLineConfigurationProvider の switchMappings は、スイッチ プレフィックスの - または -- のみを許可します。スイッチ パラメーターとしてのスラッシュ (/) も許可されません。そのため、スラッシュ スイッチの別名をスイッチ マッピングを通じて使用することはできません。
    • CommandLineConfigurationProvider では、スイッチ ベースのコマンド ライン引数 (代入値を含まない引数) を使用できません。たとえば、"/Maximize" というキーを指定することはできません。
    • Main の引数を新しい CommandLineConfigurationProvider インスタンスに渡すことはできますが、Environment.GetCommandLineArgs はプロセス名を削除してからでないと渡すことはできません (Environment.GetCommandLineArgs は、デバッガーがアタッチされていると、異なる動作をします。具体的には、デバッガーがアタッチされていない場合は、スペースを含む実行可能ファイル名は別の引数に分けられます。itl.ty\GetCommandLineGotchas (英語) を参照してください)。
    • 対応するスイッチ マッピングがないコマンド ライン スイッチ プレフィックスの - または -- を指定すると、例外が返されます。
  • 構成を更新 (Configuration["Profile:UserName"]="Inigo Montoya") できますが、更新後の値がオリジナルのストアに書き戻されることはありません。たとえば、JSON プロバイダー構成値を割り当てても、JSON ファイルは更新されません。同様に、環境変数の構成項目を割り当てても、環境変数は更新されません。
  • EnvironmentVariableConfigurationProvider では、オプションでキー プレフィックスを指定できます。その場合、指定したプレフィックスを含む環境変数のみが読み込まれます。このようにして、構成エントリの範囲を環境変数の "セクション" 内のエントリ、またはより幅広く、アプリケーションに関係あるエントリに自動的に限定できます。
  • 区切り記号としてコロンを使用する環境変数がサポートされます。たとえば、コマンド ラインで「SET AppConfiguration:ConnectionString=Console」を割り当てることができます。
  • すべての構成キー (名前) は大文字と小文字が区別されません。
  • 各プロバイダーは、専用の NuGet パッケージ内にあります。NuGet パッケージ名は次のとおり、プロバイダーに対応しています。Microsoft.Extensions.Configuration.CommandLine、Microsoft.Extensions.Configuration.EnvironmentVariables、Microsoft.Extensions.Configuration.Ini、Microsoft.Extensions.Configuration.Json、および Microsoft.Extensions.Configuration.Xml。

オブジェクト指向構造について

構成 API のモジュール性とオブジェクト指向構造はいずれも、よく考えられたものです。作業に利用できる、検出可能で、モジュール化された、簡単に拡張できるクラスとインターフェイスが提供されます (図 5 参照)。

Configuration Provider Class Model
図 5 構成プロバイダー クラス モデル

構成メカニズムの各種類には、対応する構成プロバイダー クラスがあり、それぞれ IConfigurationProvider を実装しています。組み込みプロバイダー実装の大部分では、インターフェイス メソッドのすべてのカスタム実装を使用しなくても、ConfigurationBuilder から派生することで、実装を効率よく始めることができます。驚かれるかもしれませんが、図 1 のどのプロバイダーへの直接参照もありません。それは、手動で各プロバイダーのインスタンスを作成して ConfigurationBuilder クラスの Add メソッドで登録する代わりに、各プロバイダーの NuGet パッケージに IConfigurationBuilder 拡張メソッドを持つ静的拡張クラスが含まれているからです (この拡張クラスの名前は通常、ConfigurationExtensions というサフィックスで識別できます)。拡張クラスを使用して、ConfigurationBuilder (IConfigurationBuilder を実装) の構成データに直接アクセスを開始し、プロバイダーに関連した拡張メソッドを直接呼び出すことができます。たとえば、JsonConfigurationExtensions クラスは、AddJsonFile 拡張メソッドを IConfigurationBuilder に追加し、ConfigurationBuilder.AddJsonFile(fileName, optional).Build(); への呼び出しを使用して JSON 構成を追加できるようにします。

ほとんどの場合、構成を取得したら、後は値の取得を始めるだけです。

IConfiguration には、キーを使用して目的の要素にアクセスして、特定の構成値を取得できるようにする文字列インデクサーが含まれています。一連の設定 (セクションと呼ばれる) は、GetSection メソッドまたは GetChildren メソッドで取得できます (どちらのメソッドを使用するかは、階層の追加レベルをドリルダウンするかどうかによります)。構成要素のセクションでは、以下を取得できます。

  • キー: 名前の最後の要素。
  • パス: ルートから現在の場所までを指定するフルパス名。
  • 値: 構成設定に格納される構成値。
  • オブジェクトとしての値: ConfigurationBinder を通じて取得でき、アクセスしている構成セクション (および場合によってはその子) に対応する POCO オブジェクト。これは、たとえば 図 3 の Configuration.Get<AppConfiguration>(nameof(App­Configuration)) で行っている処理です。
  • IConfigurationRoot には、構成を更新するために値を再読み込みする Reload 関数が含まれます。ConfigurationRoot (IConfigurationRoot を実装) には、再読み込み (および値の変更) が発生する場合の通知を登録できる GetReloadToken メソッドがあります。

暗号化された設定

場合によっては、クリア テキストで格納された設定ではなく、暗号化された設定が取得されるよう希望することがあります。たとえば、OAuth アプリケーションのキーやトークンを格納する場合や、データベース接続文字列の資格情報を格納する場合に、このような暗号化が重要になります。さいわい、Microsoft.Extensions.Configuration システムでは、暗号化された値の読み取りに関する組み込みのサポートがあります。セキュアなストアにアクセスするには、Microsoft.Extensions.Configuration.User­Secrets NuGet パッケージへの参照を追加する必要があります。これを追加したら、新しい IConfigurationBuilder.AddUserSecrets 拡張メソッドを使用します。このメソッドは、(project.json ファイルに格納されている) userSecretsId という構成項目文字列引数を受け取ります。UserSecrets 構成を構成ビルダーに追加すると、設定に関連付けられたユーザーのみがアクセスできる暗号化された値の取得を開始できます。

もちろん、値を設定できなければ、設定の取得にあまり意味はありません。設定には、次のように user-secret.cmd ツールを使用します。

user-secret set <secretName> <value> [--project <projectPath>]

--project オプションを使用すると、この設定を、(ASP.NET 5 の新しいプロジェクト ウィザードで既定で作成される) project.json ファイルに格納された userSecretsId の値に関連付けることができます。user-secret ツールがない場合は、開発者コマンド プロンプトで DNX ユーティリティ (現在の名前は dnu.exe) を使用して追加する必要があります。

ユーザー シークレット構成オプションの詳細については、Rick Anderson と David Roth による記事「アプリケーション シークレットの安全な格納」(bit.ly/1mmnG0L、英語) を参照してください。

まとめ

.NET を長年使用してきた開発者は、System.Configuration による構成の組み込みサポートに失望していたことでしょう。従来の ASP.NET 出身の開発者であれば、なおさらだと思います。ASP.NET では、構成は Web.Config と App.config ファイルに限られており、そのファイルの AppSettings ノードにアクセスするだけでよかったのですから。さいわい、新しいオープン ソースの Microsoft.Extensions.Configuration API では、多数の新しい構成プロバイダーと、必要な任意のカスタム プロバイダーにプラグイン可能な容易に拡張できるシステムを追加することで、本来利用できた機能をはるかに超える機能を利用できるようになっています。ASP.NET 5 以前の環境でまだ仕事をしている (身動きが取れない?) 開発者にとって、古い System.Configuration API は依然機能しますが、(併用することになるとしても) 新しいパッケージを参照して新しい API への移行を徐々に開始することができます。さらに、NuGet パッケージは、コンソールなどの Windows クライアント プロジェクトや Windows Presentation Foundation アプリケーションから使用できます。そのため、次回構成データにアクセスする必要があるときには、Microsoft.Extensions.Configuration API を利用しない手はありません。


Mark Michaelis は、IntelliTect の創設者で、同社でチーフ テクニカル アーキテクト兼トレーナーを務めています。20 年にわたって Microsoft MVP の一員であり、2007 年から Microsoft Regional Director を務めています。また、C#、Microsoft Azure、SharePoint、Visual Studio ALM など、マイクロソフト ソフトウェアの設計レビュー チームにもいくつか所属しています。開発者を対象としたカンファレンスで講演を行い、多数の書籍を執筆しています。最近では、『Essential C# 6.0 (5th Edition)』(Addison-Wesley Professional、2015 年) を執筆しました (itl.tc/EssentialCSharp、英語)。連絡先は、Facebook (facebook.com/Mark.Michaelis、英語)、ブログ (IntelliTect.com/Mark、英語)、Twitter (@markmichaelis、英語)、または電子メール mark@IntelliTect.com (英語のみ) です。

この記事のレビューに協力してくれた IntelliTect の技術スタッフの Grant Erickson、Derek Howard、Phil Spokas、および Michael Stokesbary に心より感謝いたします。