December 2017

Volume 32 Number 12

Cutting Edge - ASP.NET Core アプリケーションの構成

Dino Esposito | December 2017

Dino Esposito現実のソフトウェア アプリケーションのロジックは、外部構成データによって決まる場合があります。つまり、アプリケーションの全体の動作が、外部構成データがフェッチされたときに駆動されます。一般的には、3 種類の構成データがあります。1 つは一度だけフェッチされ、すべての場所で使用される構成データ。1 つは何度もフェッチされ、すべての場所で使用される構成データ。1 つは、使用する直前にオンデマンドでフェッチされる構成データです。2 つ目と 3 つ目の構成データは、ほとんどの場合、アプリケーション固有のデータです。1 つ目のアプリケーションの有効期間内に一度だけフェッチされる構成データの多くは、できる限り実際のデータ ストアがわからないように、ラッパー API がビルドされます。

従来の ASP.NET では、他にもさまざまな方法がある中で、アプリケーション単位の構成データのリポジトリとして web.config ファイルが好んで使用されてきました。ASP.NET Core では web.config ファイルはなくなりました。ただし、構成 API が以前よりも豊富で柔軟になっています。

構成データ プロバイダー

データ プロバイダーの概念の中心にあるのが構成 API です。データ プロバイダーは特定のソースからデータを取得し、名前と値のペアの形式でアプリケーションにそのデータを公開します。ASP.NET Core には、テキスト ファイル (中でも注目すべきは JSON ファイル) から読み取ることができる事前定義された構成プロバイダー、環境変数、インメモリ ディクショナリが含まれます。構成プロバイダーは、アプリケーションの起動時に ConfigurationBuilder クラスのサービスを介してシステムに接続されます。ConfigurationBuilder クラスにリンクされるすべてのプロバイダーは、名前と値のペアを作成し、そのコレクションを IConfigurationRoot オブジェクトとして公開します。

データは常に名前と値の形をとりますが、結果として作成される構成オブジェクトの全体構造は階層構造になることがあります。これはすべて、名前に関連付けられる値の実際の性質と構造によって決まります。スタートアップ クラスのコンストラクターの次のコード スニペットは、2 つの異なる構成プロバイダーを組み合わせる構成ツリーの作成方法を示しています。

// Env is a reference to the IHostingEnvironment instance
// that you might want to inject in the class via ctor
var dom = new ConfigurationBuilder()
  .SetBasePath(env.ContentRootPath)
  .AddJsonFile("MyAppSettings.json")
  .AddInMemoryCollection(new Dictionary<string, string> {{"Timezone", "+1"}})
  .Build();

AddJsonFile 拡張メソッドは、指定された JSON ファイルに格納されているプロパティから名前と値のペアを追加します。JSON ファイルを相対パスで指定していることに注意してください。SetBasePath メソッドは、実際には、システムがこうした参照ファイルの検索を開始するルート ディレクトリを設定します。任意の JSON ファイルを構成データのソースとして使用できます。ファイルの構造は完全にユーザーが指定でき、あらゆるレベルの入れ子を含めることができます。複数の JSON ファイルを同時にリンクできます。

AddInMemoryCollection メソッドは、指定されたディクショナリのコンテンツを追加します。ディクショナリのキーと値の型はどちらも文字列でなければなりません。一見、このような拡張メソッドは、コンパイル時にのみ設定できる静的データを追加するだけなので役立ちそうにないと思われるかもしれません。しかし、インメモリ コレクションでは、パラメーター データを分離でき、ASP.NET Core configuration API によってそのデータをアプリケーション本体から切り離して以下のように起動時に挿入することができます。

new ConfigurationBuilder()
  .AddInMemoryCollection(
    new Dictionary<string, string> {{"Timezone", "+1"}})
  .Build();

上記のコード スニペットに示すように、たとえば、使用するタイムゾーンを示す値が構成ビルダーに追加され、アプリケーションの他の部分は構成 API の統合インターフェイスを通じてその値を受け取ります。そのため、他のデータ ソースからタイムゾーン (およびメモリから挿入したその他のデータ) を読み取るために変更を加えるのは、プロバイダーと実際のストレージ パターンだけになります。

最後に、AddEnvironmentVariable メソッドはサーバー インスタンスで定義済みの環境変数を構成ツリーに追加します。定義済みの環境変数はすべて、1 つのブロックとしてツリーに追加されます。フィルター処理が必要な場合は、インメモリ プロバイダーを選択し、選択した変数のみをディクショナリにコピーするのが最適です。

また、ASP.NET Core 2.0 では IConfiguration をスタートアップ クラスのコンストラクターに挿入し、環境変数および appsettings.json と appsettings.development.json という 2 つの JSON ファイルのコンテンツを使って構成ツリーを自動構成できます。別の名前の JSON ファイルまたは別のプロバイダーのリストを使用する場合は、次のようにゼロから構成ツリーを作成します。

var dom = new ConfigurationBuilder()
  .SetBasePath(env.ContentRootPath)
  .AddJsonFile("MyAppSettings.json")
  .AddInMemoryCollection(new Dictionary<string, 
    string> {{"Timezone", "+1"}})
  .Build();

カスタム構成プロバイダー

事前定義されたプロバイダーを使用する代わりに、独自の構成プロバイダーを作成することも可能です。カスタム構成プロバイダーをビルドするには、IConfigurationSource を実装するプレーンなラッパー構成ソース クラスから始めます。以下に例を示します。

public class MyDatabaseConfigSource : IConfigurationSource
{
  public IConfigurationProvider Build(IConfigurationBuilder builder)
  {
    return new MyDatabaseConfigProvider();
  }
}

前述のコード スニペットに示した Build メソッドの実装で、最後に実際のプロバイダー、つまりシステム定義の Configuration­Provider クラスを継承するクラスを参照します (図 1 参照)。

図 1 データベース駆動型構成プロバイダーのサンプル

public class MyDatabaseConfigProvider : ConfigurationProvider
{
  private const string ConnectionString = "...";
  public override void Load()
  {
    using (var db = new MyDatabaseContext(ConnectionString))
    {
      db.Database.EnsureCreated();
      Data = !db.Values.Any()
        ? GetDefaultValues(db)
        : db.Values.ToDictionary(c => c.Id, c => c.Value);
    }
  }
  private IDictionary<string, string> GetDefaultValues (MyDatabaseContext db)
  {
    // Pseudo code for determining default values to use
    var values = DetermineDefaultValues();
    // Save default values to the store
    // NOTE: Values is a DbSet<T> in the DbContext being used
    db.Values.AddRange(values);
    db.SaveChanges();
    // Return configuration values
    return values;
  }
}

よく使われる例は、アドホックのデータべース テーブルから読み取る構成プロバイダーです。このプロバイダーでは最終的にテーブルのスキーマと関連するデータベースのレイアウトを隠すことができます。たとえば、接続文字列をプライベートな定数値にできます。このような構成プロバイダーは Entity Framework (EF) Core を使用してデータ アクセス タスクを実行する可能性が高いため、後で文字列/文字列のディクショナリに変換される値をフェッチするための専用の DbContext クラスと一連の専用のエンティティ クラスが必要になります。改善のために、見つかることが予想される値の既定値を定義し、その値が空の場合にデータベースを作成することをお勧めします。

ここで説明しているデータベース駆動型プロバイダーは、よく知られているデータベース テーブルに似ています。ただし、DbContextOptions オブジェクトを引数としてプロバイダーに渡す方法を見つけた場合は、非常に汎用的な EF ベースのプロバイダーを利用することも可能です。この手法の詳細については、bit.ly/2uQBJmK を参照してください。

構成ツリーのビルド

結果として生じる構成ツリー (個人的には構成ドキュメント オブジェクト モデルと呼んでいます) は、一般的にスタートアップ クラスのコンストラクターでビルドします。ConfigurationBuilder クラスの Build メソッドの出力は、IConfigurationRoot 型のオブジェクトです。このスタートアップ クラスは以下に示すように、アプリケーション スタック全体を通して後で使用するインスタンスを保存するためのメンバーを提供します。

public class Startup
{
  public IConfigurationRoot Configuration { get; }
  public Startup(IHostingEnvironment env)
  {
    var dom = new ConfigurationBuilder()
      .SetBasePath(env.ContentRootPath)
      .AddJsonFile("MyAppSettings.json")
      .Build();
     Configuration = dom;
  }
}

IConfigurationRoot オブジェクトは、構成ツリーの個別の値を読み取るアプリケーション コンポーネントの接続ポイントになります。

構成データの読み取り

プログラムで構成データを読み取るには、IConfigurationRoot オブジェクトの GetSection メソッドを使用します。値は、プレーンな文字列として読み取られます。読み取る正確な値を特定するには、階層スキーマのプロパティを区切るためにコロン (:) を使用するパス文字列を指定します。ASP.NET Core プロジェクトに図 2 に示すような JSON ファイルがあるとします。

図 2 JSON ファイルのサンプル

{
  "ApplicationTitle" : "Programming ASP.NET Core",
  "GeneralSettings" : {
    "CopyrightYears" : [2017, 2018],
    "Paging" : {
      "PageSize" : 20,
      "FreezeHeaders" : true
    },
    "Sorting" : {
      "Enabled" : true
    }
  }
}

設定はさまざまな方法で読み取ることができますが、構成ツリーで実際の値へ移動する方法を把握していなければなりません。たとえば、ページの既定サイズが保存されている場所を見つける場合、パスは以下のようになります。

Generalsettings:Paging:PageSize

パス文字列では常に、大文字と小文字が区別されることに注意してください。これを踏まえると、設定を読み取る最も簡単な方法は、以下のようにインデクサー API を使用する方法です。

var pageSize = Configuration["generalsettings:paging:pagesize"];

インデクサー API は設定値を文字列として返しますが、厳密に型指定される API を使用することもできます。この手法を以下に示します。

var pathString = "generalsettings:paging:pagesize";
var pageSize = Configuration.GetValue<int>(pathString);

読み取り API は実際のデータ ソースとは無関係です。インメモリ ディクショナリからフラットなコンテンツを読み取る際に使用したのと同じ API を使用して JSON から階層コンテンツを読み取ります。

直接読み取りに加え、位置指定の API を利用できます。この API は、概念的に特定の構成サブツリーで読み取りカーソルを移動します。GetSection メソッドでは、インデクサー API と厳密に型指定される API の両方を使用して移動できる構成サブツリー全体を選択できます。GetSection メソッドは、構成ツリー用の汎用的なクエリ ツールで、JSON ファイル固有ではありません。以下に例を示します。

var pageSize = Configuration.GetSection("Paging").GetValue<int>("PageSize");

読み取りでは、GetValue メソッドと Value プロパティも使用できます。どちらも、設定値をプレーンな文字列として返します。

読み込み済みの構成の更新

ASP.NET Core では、構成 API は読み取り専用として設計されています。これは、正規の API を使って、構成したデータ ソースを書き戻すことができないことを意味します。データ ソースのコンテンツを編集する (つまり、プログラムによるテキスト ファイルの上書き、データベースの更新など) 手段があれば、システムではプログラムから構成ツリーを再読み込みすることはできます。

構成ツリーを再度読み込むには、構成ルート オブジェクトの Reload メソッドを呼び出すだけです。

Configuration.Reload();

通常は、保存された設定を更新するためのフォームをユーザーに提供する管理ページ内からこのコードを使用します。JSON ファイルに関しては、コンテンツの変更時に自動的に読み込むこともできます。次のように、AddJsonFile に追加のパラメーターを加えるだけです。

var dom = new ConfigurationBuilder()
  .AddJsonFile("MyAppSettings.json", optional: true, reloadOnChange: true);

JSON ファイルは、ASP.NET Core でネイティブにサポートされるテキスト ファイル形式の中で最も人気があります。また、XML や .ini ファイルから設定を読み込むこともできます。AddJsonFile と同じ多くのシグネチャを持つ AddXmlFile メソッドと AddIniFile メソッドへの呼び出しを追加するだけです。

ASP.NET Core 2 では、次に示すように、program.cs ファイルから構成を管理できます。

return new WebHostBuilder()
  .UseKestrel()
  .UseContentRoot(Directory.GetCurrentDirectory())
  .ConfigureAppConfiguration((builderContext, config) =>
  {
    var env = builderContext.HostingEnvironment;
    config.AddJsonFile("appsettings.json")
  });

このように管理する場合、コンストラクターの IConfiguration によって、スタートアップ クラスに構成ツリーを挿入できます。

構成データの受け渡し

構成ルート オブジェクトはスタートアップ クラスのスコープ内に存在しますが、そのコンテンツはアプリケーション全体で使用できなくてはなりません。ASP.NET Core でこれを行うには、依存関係の挿入 (DI) を利用するのが自然な方法です。システムと構成ルート オブジェクトを共有するには、このオブジェクトをシングルトンとして DI システムにバインドします。

public void ConfigureServices(IServiceCollection services)
{
  services.AddSingleton(Configuration);
  ...
}

その後、任意のコントローラーのコンストラクターや Razor ビューに IConfigurationRoot 参照を挿入できます。以下に例を示します。

@inject IConfigurationRoot Configuration
CURRENT PAGE SIZE IS @Configuration["GeneralSettings:Paging:PageSize"]

構成ルート オブジェクトを挿入することも可能で、ほとんどの場合簡単に行えます。しかし、構成設定へのアクセスが頻繁に行われる場合、API が非常に厄介なものになります。そのため、ASP.NET Core 構成 API は、構成ツリーの全体または一部を独立した POCO クラスに保存できるシリアル化メカニズムを提供しています。

POCO クラスへのシリアル化

これまでに何らかの ASP.NET MVC プログラミングを行ったことがあれば、モデル バインディングの概念を理解されていることでしょう。ASP.NET Core では、Options パターンという類似パターンを使用して、以下に示すようにモデル バインディングを行うことができます。

public void ConfigureServices(IServiceCollection services)
{
  // Initializes the Options subsystem
  services.AddOptions();
  // Maps the PAGING section to a distinct dedicated POCO class
  services.Configure<PagingOptions>(
    Configuration.GetSection("generalsettings:paging"));
}

この例では、まず上記のコード スニペットに示すように構成バインディング レイヤーを初期化します。次に、以下に示すように指定されたサブツリーのコンテンツを特定のクラスのパブリック プロパティにバインドするように明示的に要求します。

public class PagingOptions
{
  public int PageSize { get; set; }
  public bool FreezeHeaders { get; set; }
}

バインド クラスは、getter と setter のパブリック プロパティと既定のコンストラクターを含む POCO クラスでなくてはなりません。Configure メソッドは、構成ツリーの指定されたセクションからクラスの新しく作成されたインスタンスへの値のフェッチとコピーを試みます。言うまでもなく、宣言された型に値を変換できない場合、エラー メッセージが表示されることなく、バインディングは失敗します。

このような POCO クラスは、組み込みの DI システムを使用して、アプリケーション スタック全体に渡すことができます。このために、何かを行う必要はありません。むしろ、必要な構成は、AddOptions の呼び出し時に適用されます。クラスにシリアル化された構成データにプログラムからアクセスするには、もう 1 つ手順が残っています。

public PagingOptions PagingOptions { get; }
public CustomerController(IOptions<PagingOptions> config)
{
  PagingOptions = config.Value;
}

すべてのコントローラーで Options クラスを幅広く使用する場合、オプション プロパティ (Paging­Options) を基本クラスに移動し、そこからコントローラー クラスを継承することをお勧めします。同様に、任意の Razor ビューに IOptions<T> オブジェクトを挿入できます。

まとめ

従来の ASP.NET MVC では、構成データに対処するベスト プラクティスは、起動時にすべてのデータを一度にグローバル コンテナー オブジェクトに読み込むことです。その後、グローバル オブジェクトにコントローラー メソッドからアクセスし、そのコンテンツをリポジトリやビューなどのバックエンド クラスに引数として挿入します。従来の ASP.NET MVC では、柔軟な文字列ベースのデータをグローバル コンテナーの厳密に型指定されるプロパティにマッピングする作業のすべてにユーザーが対処する必要がありました。

ASP.NET Core では、従来の ASP.NET の ConfigurationManager API よりもずっと正確に個々の値を読み取る低レベルの API と、外部コンテンツを設計の POCO クラスに自動的にシリアル化する方法の両方を利用できます。これは、プロバイダーのリストによって設定される構成ツリーの導入によって、構成をアプリケーションの本体から切り離すことによって実現します。


Dino Esposito は、『Microsoft .NET: Architecting Applications for the Enterprise』(Microsoft Press、2014 年) および『Programming ASP.NET Core』(Microsoft Press、2018 年) の著者です。Pluralsight の著者であり、JetBrains のデベロッパー アドボケイトでもある Esposito は、Twitter (@despos、英語のみ) でソフトウェアに関するビジョンを紹介しています。

この記事のレビューに協力してくれたマイクロソフト技術スタッフの Ugo Lattanzi に心より感謝いたします。
Ugo は、Web アプリケーション、サービス指向型アプリケーション、スケーラビリティを最優先する環境に重点を置きながら、さまざまなツールと言語を用いてエンタープライズ アプリケーションを開発することを専門とするプログラマです。Ugo は 9 年連続で Microsoft MVP を受賞しています。


この記事について MSDN マガジン フォーラムで議論する