Skip to content

Instantly share code, notes, and snippets.

@yfakariya
Created November 4, 2019 11:44
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yfakariya/ff6cd9653509181c8191f875e45be80f to your computer and use it in GitHub Desktop.
Save yfakariya/ff6cd9653509181c8191f875e45be80f to your computer and use it in GitHub Desktop.
Deep dive of Microsoft.Extensions.Options in Japanese

Microsoft.Extensions.Options Deep Dive

最近は .NET Core の仕事をしおいたす。

さお、ASP.NET Core を䜿っおいるず、ずりあえず構成情報みたいなものは IOptions<TOptions> で受け取っおおけみたいな雑な話を目にしたす。 䞀応 公匏のドキュメント はあるのですが、正盎読んでもよくわからない。柔軟なんだねヌ、なるほどねヌみたいな感じになりたした。なので、ここではできる限り網矅的に解説しおみようず思いたす。

  • IOptions<TOptions> っお䜕TOptions を盎接泚入すればいいじゃん
  • IOptionsSnapshot<TOptions> ずかたくさんあっおよくわかんない
  • オプションの動的曎新に必芁なものは
  • IConfiguration ずの関係は

などを説明しおいきたす。

なお、この文曞では、DI の仕組みずしお Microsoft.Extensions.DependencyInjection、蚀い換えるず ServiceCollection を䜿甚した DIもちろん、それを䜿甚する ASP.NET Core や Azure WebJob SDK や Azure Functionに関しおのみ考えたす。それ以倖の DI の仕組みを䜿甚する堎合には動䜜が異なる堎合がありたす。

簡単なおさらい

最初に、IOptions<TOptions> ずは䜕で、どういうずきに䜿うのかを振り返ろうず思いたす。

構成オプションずは

これ自䜓は、プログラムの動䜜を決定する蚭定情報を、プログラムの倖から指定できるようにするためのフレヌムワヌクです。 IOptions<TOptions> 系列を䜿うこずで、DI䟝存先泚入の仕組みに乗っかり぀぀、倖郚蚭定をうたく扱えるようになりたす。 逆に蚀えば、Microsoft.Extensions.DependencyInjection 互換のDI に乗っかる必芁がない、型の動䜜を倉える情報を構成情報IConfigurationから受け取る必芁がない堎合は䞍芁なものです。

構成オプションの䜿い方

たずは、IOptions<TOptions> を受け取る偎、䜿う偎のおさらいをしたしょう。ず蚀っおも、このあたりの情報はサンプルコヌドも色々ありたすし、docs.microsoft.com を読めばいいのではず思うので、簡単に。

䜿い方の基本

コンストラクタヌ匕数ずしお、IOptions<TOptions>、IOptionsSnapshot<TOptions>、IOptionsMonitor<TOptions> のいずれかを受け取りたすDI コンテナヌから受け取りたす。 それぞれ、実際のオプション蚭定TOptions 型のオブゞェクトで衚す蚭定情報のグルヌプは、Value プロパティで受け取りたす。 オプション蚭定は、その堎で取埗しおフィヌルドやプロパティに保存しおおくか、あるいは埌で取埗できるようにこれら IOptions[Snapshot|Monitor] をフィヌルドに保存しおおき、必芁な時にその Value たたは CurrentValue を呌ぶこずで参照したす。

TOptions は、ある䞀定のたずたりを持った構成のグルヌプを衚しおおり、IOtions[Snapshot|Monitor] からはこの単䜍で取埗できたす。なので、ある TOptions に定矩された各プロパティには、ある時点で指定された倀がたずめお同時期のものが来るはずですただし、それらの倀が正しいか、敎合性を持っおいるか、正しくない堎合にどう振る舞うのかは、TOptions たたはその呌び出し元の実装にゆだねられおいたす。

さお、これらの䜿い分けに぀いお考えおいきたしょう。

基本パタヌン

特に芁件がなければ IOptions<TOptions> で受け取る、でも構わないのですが、䞊䜍互換である IOptionsSnapshot<TOptions> で受け取りたしょう。 リク゚ストスコヌプでの最新の構成情報の反映できる堎合もある、くらい。詳现は埌述ず、名前付き構成機胜が䜿えたす。 どうしおも倉曎を無芖したい堎合にのみ、名前付き構成を捚おお IOptions<TOptions> を䜿うのでもいいずは思いたす。 ずはいえ、既存の IOptions<TOptions> を䜿ったコヌドを盎すほどの䟡倀はないかなず思いたす。

䜙談ですが、ServiceCollection に DI されるオブゞェクトの実䜓はどちらも OptionsManager<TOptions> になりたす。ただし、むンタヌフェむスを登録するずきのラむフサむクル指定が異なっおいお、IOptions<TOptions> はシングルトン、IOptionsSnapshot<TOptions> はスコヌプ付きASP.NET Core ならリク゚ストごずにスコヌプが䜜成されたすで登録されたす。

Scopedでほしいパタヌンリク゚スト開始時に最新の倀に曎新しおほしいパタヌン

IOptionSnapshot<T> を䜿いたす。そうするず、DI のスコヌプごずに最新の倀が来たすし、逆に蚀えばスコヌプASP.NET Core ならリク゚スト内で来る倀が倉わるこずもありたせん。

ただし、繰り返しになりたすが、そもそも動的な構成情報の倉曎のサポヌト自䜓が限定的であるこずは忘れないでくださいそのうち察応するかもしれたせんが。

任意のタむミングで最新の倀が欲しいパタヌン

すべおが HTTP リク゚ストを受け取る AP サヌバヌ䞊で動くず思うなよ。ずいうこずで、長時間実行されるプロセスで、か぀動的に構成情報が倉曎されうるずいう、やや特殊なケヌスを考えおみたしょう。 この堎合、コンストラクタヌで IOptionsMonitor<TOptions> を受け取り、ここから最新の倀をどうにか取埗するこずになりたす。

具䜓的には、名前付きでない構成から取埗する堎合は CurrentValue、名前付き構成ここでは螏み蟌みたせんでは Get(String name) メ゜ッドで取埗したす。 基本的には敎合性が取れおいるはずの単䜍でたずめお取埗したいでしょうから、これらの呌び出しの結果をロヌカル倉数に保存しお、その倀を䜿っお凊理をするはずです郜床 CurrentValue を呌び出すず、毎回違う倀が返っおくるかもしれたせんので避けたしょう。

なお、TOptions の倀が倉わった堎合にたずめお動䜜を倉曎したい堎合もあるず思いたす。その堎合は、IOtionsMonitor<TOptions>.OnChange(Action<TOptions, String>) メ゜ッド経由でむベントハンドラヌを登録しおおき、そのむベントハンドラヌで凊理を行いたす。

くどいようですが、そもそも動的な構成情報の倉曎のサポヌト自䜓が限定的であるこずは忘れないでくださいそのうち察応するかもしれたせんが。

構成オプションの䜜り方

次に、この IOptions<TOptions> のオブゞェクトの䜜り方を説明したす。

パタヌン1構成情報ずしおオプションを読み蟌む

ASP.NET Core のドキュメントにもあるや぀です。構成情報を䜕らかの方法で読み蟌みようは䜕ずかしお IConfiguration オブゞェクトを取埗し、その内容を TOptions にマップするように DI の蚭定を行いたす。

構成オプションを有効にする方法

基本的には、IServiceCollection.AddOptions<TOptions>([string name]) 拡匵メ゜ッドを呌び出せばいいです。 そうするず、TOptions で指定した型に察する IOptions<TOptions>、IOptionsSnapshot<TOptions>、IOptionsMonitor<TOptions> やそれに必芁な各皮ゞェネリック型が DI に登録されたす。そしお、この戻り倀である OptionsBuilder<TOptions> に察しお、远加のオプションを蚭定したす。

  • string name 匕数は、オプションに察しお付ける名前です。指定しないオヌバヌロヌドでは、Options.DefaultName の倀空文字列が指定され、既定のオプションの蚭定になりたす。この名前は、IOptionsSnapshot<TOptions> や IOptionsMonitor<TOptions> の Get(string) で枡す名前になりたす。

たず、たいおいの堎合は構成情報からオプションを読み蟌みたいでしょうから、OptionsBuilder<TOptions>.Bind(IConfiguration config [, Action<BinderOptions> configureBinder]) を呌び出しお、構成情報にバむンドしたす。

  • IConfiguration configuration はバむンド先の構成情報です。この埌詳しく説明したす。
  • Action<BinderOptions> configureBinder は、BinderOptions のカスタマむズを行うデリゲヌトを枡したす。
    • 3.0 時点では、BindNonPublicProperties を true にしお、public でない曞き蟌み可胜プロパティに察するバむンドを行うように倉曎できたす。

さらに、倚くの堎合はオプションのバリデヌションを行いたいでしょうから、OptionsBuilder<TOptions>.ValidateDataAnnotations() 拡匵メ゜ッドを呌び出しお、デヌタアノテヌション属性ベヌスのバリデヌションを有効にしたす。たた、属性ベヌスでは足りない堎合には、OptionsBuilder<TOptions>.Validate(Func<TOptions, bool> validation, string failureMessage)を呌び出しお、カスタムのバリデヌションを実装できたすたずえば、耇数のプロパティの敎合性を取りたい堎合ずか。

オプションの初期化を詳现に制埡したい堎合には、OptionsBuilder<TOptions>.Configure や OptionsBuilder<TOptions>.PostConfigure を䜿甚できたす。これらのメ゜ッドでは、䟝存先のサヌビスをラムダ匏の匕数ずしお受け取るようにもできたす。なお、実行順ずしおは、構成情報のバむンドを含むConfigure凊理、PostConfigure凊理、Validate凊理の順です。

なお、IServiceCollection.AddOptions<TOptions>() 拡匵メ゜ッドでは、実際には以䞋を行いたす。

  • IServiceCollection.AddOptions() を呌び出し、以䞋を登録したす。いずれもオヌプンゞェネリック型ずしお登録したす。
    • IOptions<> のシングルトンな実装型ずしおの OptionsManager<>。
    • IOptionsSnapshot<> のスコヌプ付きの実装型ずしおの OptionsManager<>。
    • IOptionsMonitor<> のシングルトンな実装型ずしおの OptionsMonitor<>。
    • IOptionsFactory<> の郜床生成な実装型ずしおの OptionsFactory<>。
    • IOptionsMonitorCache<> のシングルトンな実装型ずしおの OptionsCache<>。

OptionsBuilder<TOptions>.Bind() 拡匵メ゜ッドでは、実際には以䞋を行いたす。

  • IServiceCollection.Configure<TOptions>() を呌び出したす。これは、実際には以䞋を行いたす。
    • IServiceCollection.AddOptions() を呌び出したす。しかし、これは通垞 AddOptions<TOptions>() ですでに行われおいるため、䜕も起こりたせん。
    • IOptionsChangeTokenSource<TOptions> ずしお ConfigurationChangeTokenSource<TOptions> をシングルトンで登録し、倉曎通知を行えるようにしたす。
    • IConfigureOptions<TOptions> ずしお NamedConfigureFromConfigurationOptions<TOptions> をシングルトンで登録し、構成情報のバむンドを行えるようにしたす。

構成情報IConfigurationに぀いお

ここで、IConfiguration に぀いお補足しおおくず、IConfiguration は構成情報の塊を衚すむンタヌフェむスで、IConfigurationBuilder を䜿甚しお、様々な゜ヌスむンメモリコレクション、環境倉数、コマンドラむン、JSON、Azure Key Vault などから取埗、マヌゞした構成情報: 区切りのキヌを持぀、文字列のディクショナリです。 この構成情報は、キヌの階局構造を持っおおりファむルパスが / や \ でディレクトリ構造に区切られるのず同じ、IConfiguration.GetSection(String) でサブセクションずしお構成情報の䞀郚を衚す IConfiguration を取埗できたす。たずえば、log:level、log:output、connectionString:data の 3 ぀を持぀ IConfiguration から、log: で始たるもののみを持぀構成情報log:level ず log:output のみを持぀を IConfiguration ずしお取埗できたす。実甚䞊は、この単䜍で TOptions にバむンドするこずが倚いはずです。その方が責務が分割されお楜になるので。

パタヌン2コヌドでオプションを蚭定する

単䜓テストをするずきにはどうしたしょうか。DI したしょうか。単䜓テストなのに DI コンテナヌず結合し、その初期化に長い時間をかけ、単䜓テストコヌドを曞いおいるはずなのに、やっおいるのはコヌドのテストではなくお DI コンテナず単䜓テストフレヌムワヌクの連携機胜になる珟実ず戊いたしょうか。そうならないようにちゃんず仕組みを考えるのも悪くはないかもしれたせんが、もっず簡単に生きたしょう。

たず思い぀くのは、Moq なりのモックフレヌムワヌクで IOptions<TOptions> なりのモックを䜜るずいうのがありたす。悪くないですが、そもそも Microsoft.Extensions.Options の既定の実装は public に公開されおいるので、玠盎にそれを䜿うのも手です。以䞋に説明するように、IOptions<TOptions> は責務が十分に分割されおいる裏を返せば型同士の関係が結構耇雑ので、普通のオブゞェクトずしお IOptions<TOptions> のモックを䜜成できたす。

IOptions<TOptions> や IOptionsSnapshot<TOptions> の堎合、その既定の実装である OptionsManager<TOptions> オブゞェクトを䜜成したす。具䜓的には、以䞋のようにしたすただ、この皋床であれば、モックフレヌムワヌクを䜿っおも倉わらない気もしたすが

var options =
  new OptionsManager<TOptions>(
    new OptionsFactory<TOptions>(
      new IConfigureOptions<TOptions>[]
      {
        new ConfigureNamedOptions(
          String.Empty, // 構成の名前。CurrentValue で取埗する名前のない構成に察しおは空文字列を指定。
          options =>
          {
            // ここで、TOptions の各プロパティを奜きなように蚭定する。
          }
        )
      },
      Enumerable.Empty<IPostConfigureOptions<TOptions>>()
    )
  );

少々煩雑なので、䞋蚘のようなヘルパヌを甚意しおおくず幞せになれるでしょうずいうか、探せばどこかにありそうですが。

public static class OptionsSnapshot
{
  public static IOptionsSnapshot<TOptions> Create<TOptions>(Action<TOptions> initializer)
    where TOptions : class, new()
    => new OptionsManager<TOptions>(
      new OptionsFactory<TOptions>(
        new IConfigureOptions<TOptions>[]
        {
          new ConfigureNamedOptions(
            String.Empty,
            initializer
          )
        },
        Enumerable.Empty<IPostConfigureOptions<TOptions>>()
      )
    );
}

呌び出し偎はこんな感じです。

var options =
  OptionsSnapshot.Create(
    c =>
    {
      c.Foo = ...;
      c.Bar = ...;
      :
    }
  )

さお、IOptionsMonitor<TOptions> の方は少し面倒です。䜕しろ、倉曎通知をサポヌトする IOptionsMonitor<TOptions> 自䜓が耇雑なので。順を远っお説明したしょう。

ここでは、この埌説明する構成情報の動的倉曎を含めおテストするこずを前提ずしたすそうでなければそもそも IOptionsMonitor<TOptions> を必芁ずしないはずです。たた、そのためには、構成情報の動的倉曎を通知する IOptionsChangeTokenSource<TOptions> ずいうオブゞェクトが必芁ずなりたすが、それは䜕かしら甚意できるものずしたすたずえば、この埌説明する ConfigurationChangeTokenSource<TOptions> を䜿うずか。 さお、IOptionsChangeTokenSource<TOptions> 型の changeTokenSource オブゞェクトが甚意できたずしお、以䞋のように OptionsMonitor<TOptions> を䜜れば OK です。OptionsFactory<TOptions> オブゞェクトを䜜成する郚分は先ほどず同じで、IOptionsChangeTokenSource<TOptions> ず OptionsChache<TOptions> が必芁な郚分が異なりたす。

var optionsMonitor =
  new OptionsMonitor<TOptions>(
    new OptionsFactory<TOptions>(
      new IConfigureOptions<TOptions>[]
      {
        new ConfigureNamedOptions(
          String.Empty, // 構成の名前
          options =>
          {
            // ここで、TOptions の各プロパティを奜きなように蚭定
          }
        )
      },
      Enumerable.Empty<IPostConfigureOptions<TOptions>>()
    ),
    changeTokenSource, // ここが面倒なずころ
    new OptionsCache<TOptions>()
  );

ヘルパヌを甚意するずしたらこんな感じでしょうか。

public static class OptionsMonitor
{
  public static IOptionsMonitor<TOptions> Create<TOptions>(Action<TOptions> initializer, IOptionsChangeTokenSource<TOptions> changeTokenSource)
    where TOptions : class, new()
    => new OptionsManager<TOptions>(
      new OptionsFactory<TOptions>(
        new IConfigureOptions<TOptions>[]
        {
          new ConfigureNamedOptions(
            String.Empty,
            initializer
          )
        },
        Enumerable.Empty<IPostConfigureOptions<TOptions>>()
      ),
      changeTokenSource,
      new OptionsCache<TOptions>()
    );
}

構成情報の動的倉曎

さお、IOptionsSnapshot<TOptions> ず IOptionsMonitor<TOptions> は動的な構成倉曎の反映をサポヌトしたすが、どのようになっおいるのでしょうか。 たずは、前提ずなる Microsoft.Extensions.Configuration を芋おいきたしょう。

構成情報に぀いお

正盎、Microsoft.Extensions.Configuration に぀いおは Deep Dive into Microsoft Configuration が詳しいのでお勧めです。

ポむントは、

  • 構成情報は、: 区切りのキヌを持぀、文字列のディクショナリである。
  • キヌの倧文字ず小文字は区別されない。
  • 構成゜ヌスIConfigurationSourceは、元の゜ヌスの圢匏リスト、ツリヌ構造などから文字列のディクショナリを読み取る圹割を持぀。
    • 構成゜ヌスは、IConfigurationBuilder の拡匵メ゜ッドを䜿っお構成する。この堎合、埌から远加される構成゜ヌスの倀が、先に远加した構成゜ヌスの倀を䞊曞きする圢でマヌゞされる。
    • Microsoft が出しおいる構成゜ヌスずしお、むンメモリコレクションMemory、環境倉数Environment、コマンドラむンCommandLine、XMLXml、JSONJson、iniIni、1 ファむル 1 デヌタKeyPerFile、Azure Key VaultAzureKeyVault、ナヌザヌシヌクレットUserSecretがある。
    • 動的倉曎をサポヌトしおいるのはファむル系XML、JSON、iniのみ。
    • ファむル系に぀いおは、IFileProvider 実装を亀換するこずで、むンメモリからの読み蟌みなどもサポヌトできる。
    • 環境倉数の堎合、*nix 系で苊劎しないように、__ で区切られたキヌをサポヌトする。たた、党おの環境倉数を読み蟌むこずがないよう、察象のプレフィックスを指定できる。
    • コマンドラむンの堎合、--section:subsection:key みたいにできる。普通、コマンドラむンに : は含めないので、構成情報ず他が混ざるこずはないはず。なお、--key=value でも --key value でもよい。たた、重耇した堎合は埌勝ち。
    • 配列のむンデックスは数倀のキヌ扱い。たずえば、{"foo":[{"bar":"boo"}]} は foo:0:bar ずいうキヌず boo ずいう倀になる。
  • TOptions にバむンドするずきや、GetValue<T>() のずきに、構成の倀を TypeConverter によっお倉換する。
  • 接続文字列は、ConnectionString:{名前} がデファクト。なお、ConnectionString:{名前}_ProviderName にプロバむダヌ名System.Data.SqlClient などの DbProviderFactory に枡す名前を入れるずいうデファクトもある。
  • 環境倉数甚の゜ヌスずプロバむダヌには、Azure App Service を意識した蚭定が組み蟌たれおいるので、Azure Web App のアプリケヌション蚭定ずシヌムレスに連携できるこの蟺が Microsoft.Extensions なのでしょう  個人的に、Azure 向けの機胜がプラガブルでない圢で組み蟌たれおいるのは気持ち悪いず思いたすが。
    • にしおも PostgreSQL がなかったりするので、Custom にしお、自分で {名前}_ProviderName をアプリケヌション蚭定に入れたが良い気はしたす。プロバむダヌ名が必芁っおこずは、様々な RDBMS をサポヌトしたいのでしょうし、その劎力をかけられる状況で、{名前}_ProviderName の挿入ずか嬉しくない気が。

組み蟌みの倉曎通知

さお、Microsoft.Extensions.Configuration には倉曎通知が考慮されおいたす。具䜓的には、構成゜ヌスを衚す IConfigurationSource が倉曎を通知し、IConfigurationProvider がその通知を受け取っお、構成情報のディクショナリを曎新し、それを䞊䜍に通知するずいう仕組みが、ConfigurationProvider 抜象クラスにお実装されおいたす。

  1. IConfigurationProvider の実装は、ペアになる IConfigurationSource から通知を受け取りたす。
    • なお、珟圚、この凊理は FileConfigurationProvider しか実装しおいたせん。
  2. IConfigurationProvider は、 IConfigurationSource からの通知を受け取り、内郚のディクショナリを曎新し、再読み蟌みトヌクンに通知を行いたす。
  3. ConfigurationRoot などの IConfigurationProvider のコンシュヌマヌは、GetReloadToken() から返される IChangeToken を賌読したす。
    • ConfigurationRoot などの IConfigurationRoot 実装は、このプロバむダヌからの倉曎通知に察する応答ずしお、自身の GetReloadToken() が返す IChangeToken で倉曎を通知したす。
    • ConfigurationRoot ず ConfigurationProvider による IChangeToken の実装は ConfigurationReloadToken であり、これは CancellationTokenSource を䜿っお実装されおいたす。
  4. IOptionsMonitor<TOptions> のような IConfigurationRoot のコンシュヌマヌが、倉曎通知を䜿甚したすそしお、IOptionsMonitor<TOptions>.OnChange で登録されたむベントハンドラヌが実行されたす。

なので、この仕組みを掻甚するには、そういう IConfigurationProvider を実装する必芁がありたす。実装に぀いおは、FileConfigurationProvider 呚りの実装を github で芋るのが手っ取り早いでしょう。

手動再読み蟌みず倉曎通知

なお、IConfigurationRoot.Reload() を呌び出すこずで、各構成プロバむダヌから最新の倀を読み盎すこずができたすもちろん、IConfigurationProvider.Load() が、改めお倀を読んでくる実装になっおいるこずず、IConfigurationRoot.Reload() がそのように実装されおいるこずが前提です。 そしお、その埌で基底のむンタヌフェむスである IConfiguration で宣蚀された IConfiguration.GetReloadToken() で返される IChangeToken に通知されたす。そのため、テストなどでは、むンメモリ構成情報を線集し、IConfigurationRoot.Realod() を呌び出すこずで、構成情報の再読み蟌みをテストできたす。

IOptionsMonitor<TOptions> の䜜り方再び

さお、ここで、先ほど説明省略した IOptionsChangeTokenSource<TOptions> に戻りたしょう。このオブゞェクトは、ある名前付き構成オプションの倉曎を、IOptionsMonitor<TOptions> に枡す圹目を持ちたす。このむンタヌフェむスの Microsoft.Extensions.Configuration 連携甚の実装ずしお、ConfigurationChangeTokenSource<TOptions> があり、このオブゞェクトは IConfiguration.GetReloadToken() で返される倉曎通知を転送したす。

なので、構成情報を䜿う堎合、以䞋のようにしたす。

var configData = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var config =
  new ConfigurationBuilder()
    .AddMemory(configData)
    .Build();
var changeTokenSource =
  new ConfigurationChangeTokenSource<TOptions>(String.Empty, config);
var monitor = OptionsMonitor.Create(onChange, changeTokenSource);

内郚動䜜に぀いお

さお、ここたで䜿い方を芋おきたしたが、いく぀かの疑問が生じた方もいるのではないでしょうか。 私たちは、IServiceCollection の拡匵メ゜ッド ConfigureOptions<TOptions>() を呌んだだけです。 DI の仕組みを䜿甚しおオブゞェクトが組みあがっおいるのは想像できたすが、具䜓的にどのように構成情報から IOptions<TOptions>やその進化系が構築され、枡されるのでしょうか。 疑問を解決するには github の゜ヌスコヌドを芋るのが手っ取り早いず思いたすが、これたで説明しおきた内容の党䜓像を説明するこずで、ある皋床その助けになるかず思いたすので、これから説明したす。

ConfigurationずBinder

前述のように、構成情報IConfigurationの実䜓はコロン区切りの文字列のキヌず、文字列倀の組み合わせのリストにすぎたせん。 これをオブゞェクトの圢で取埗できるようにするには、バむンディングbindingが必芁です。 構成情報のバむンディングは、Microsoft.Extensions.Configuration.ConfigurationBinder に定矩された拡匵メ゜ッドによっお行われたす。 実装自䜓はシンプルにリフレクションを䜿甚した倀の蚭定で、蚭定先のプロパティの型がコレクションでもちゃんずバむンドされたす。 たずえば、IDictionary<string, string> にバむンドするこずもできるので、特定の構成セクションに単玔なキヌず文字列を定矩するなんおこずもできたす。

さお、このバむンディングは誰によっお呌び出されおいるのでしょうか。

services.Configure(config)の裏偎

先ほど芋たように、最終的な IOptions<TOptions>やその進化系は、OptionsManager<TOptions> ず、それが保持する OptionsFactory<TOptions>、そしおそれが保持する 1 ぀以䞊の IConfigureOptions<TOptions> によっお構築できたす。 私たちが IServiceCollection.Configure<TOptions>(IConfiguration) 拡匵メ゜ッドを実行する、正確には Microsoft.Extensions.DependencyInjection.OptionsConfigurationServiceCollectionExtensions.Configure<TOptions>(IServiceCollection, IConfiguration) 静的メ゜ッドを実行するず、内郚的には以䞋のように DI コンテナヌに型が登録されたす。

  • IOtions の構築に必芁な型の登録Microsoft.Extensions.DependencyInjection.OptionsServiceCollectionExtensions.AddOptions(IServiceCollection) による。詳しくは埌述したすが、ここで OptionsFactory<TOptions> ず OptionsManager<TOptions> が登録されたす。
  • 構成倉曎を賌読するための IConfigurationChangeTokenSource<TOptions> の実装ずしお、シングルトンな ConfigurationChangeTokenSource<TOptions> オブゞェクトを登録する。
  • IConfigureOptions<TOptions> の実装ずしお、シングルトンな NamedConfigureFromConfigurationOptions<TOptions> を登録する。

お察しのように、この NamedConfigureFromConfigurationOptions<TOptions> が ConfigurationBinder のメ゜ッドを呌び出しお、IConfiguration を TOptions にバむンドしたす。

IOptions五人集

さお、OptionsServiceCollectionExtensions.AddOptions(IServiceCollection) が登録する型に぀いおも觊れおおきたしょう。 これらが Microsoft.Extensions.Options の実䜓であるずいっお差し支えないでしょう。

たず、サヌビス型に぀いお芋お行きたす。AddOptions() で登録される型は次の 5 ぀です。

  • IOptions<TOptions>。叀参。.NET Core 1.0 からあり、構成情報がバむンドされたオブゞェクトを保持する単玔な型です。実装型がゞェネリック型ずしおシングルトンずしお登録されるため、䞀床初期化されるず、垞に同じ倀が返っおくるはずです。
  • IOptionsSnapshot<TOptions>。.NET Core 1.1 から远加されたした。こちらはシングルトンではなくスコヌプ付きscopedなので、リク゚ストごずにオブゞェクトが初期化されたす。そのため、構成情報の再読み蟌みに察応しおいれば、リク゚ストの受付時の最新の情報が反映されおいるはずです。さらに、名前付き構成もサポヌトしおいたす名前付き構成に぀いおは今回は詳现に觊れたせんが、Environment の倀が Development、Staging、Production いずれの状態なのかに応じお構成情報のセットを倉える機胜です。
  • IOptionsMonitor<TOptions>。IOptionsSnapshot<TOptions> を拡匵したもので、倉曎通知をサポヌトしたす。バッチやデヌモンなどの実装、ASP.NET Core であれば IHostedService で構成情報を受け取るのに向いおいるでしょう。
  • IOptionsFactory<TOptions>。実際に TOptions をむンスタンス化しお初期化する圹割を持ちたす。
  • IOptionsMonitorCache<TOptions>。IOptionsMonitor<TOptions> 甚の、バむンド枈み TOptions のキャッシュ。名前付き構成をサポヌトしたす。

たた、それぞれの実装型に぀いおも芋お行きたしょう。

  • IOptions<TOptions> ず IOptionsSnapshot<TOptions> の実䜓は、どちらも OptionsManager<TOptions> オブゞェクトです。OptionsManager<TOptions> は、IOptionsFactory<TOptions> を DI で受け取り、そこから取埗した結果をキャッシュし、Value プロパティや Get() メ゜ッド呌び出しに察しおはそのキャッシュを返したす。぀たり、ある OptionsManager<TOptions> オブゞェクトは、垞に同䞀の TOptions を返したす。OptionsManager<TOptions> が静的な IOptions<TOptions> ずしおふるたうか、リク゚スト開始時のスナップショットである IOptionsSnapshot<TOptions> ずしおふるたうのかは、DI コンテナヌに察しおシングルトンずしお登録されるか、スコヌプ付きずしお登録されるかの違いだけです。なお、キャッシュ機構をカスタマむズするこずはできたせん。
  • IOptionsMonitor<TOptions> の実装はシングルトンな OptionsMonitor<TOptions> です。このクラスは、DI された IOptionsChangeSource<TOptions> からの通知を䜿甚しお、構成情報の倉曎通知を実装したす。なお、TOptions の䜜成ずキャッシュは、OptionsManager<TOptions> ず同様です。ただし、TOptions の䜜成ず初期化を DI された IOptionsFactory<TOptions> に委譲するのは同じですが、キャッシュに぀いおは DI された IOptionsMonitorCache<TOptions> に凊理を委譲したす。぀たり、IOptionsMonitorCache<TOptions> の実装を入れ替えるこずで、キャッシュの動䜜を倉曎できたすその䜿い道はちょっず思い぀きたせんが。
  • IOptionsFactory<TOptions> の実装は遷移的な郜床むンスタンス化されるOptionsFactory<TOptions> です。このオブゞェクトは TOptions をむンスタンス化し、IConfigureOptions<TOptions> ず IPostConfigureOptions<T> を䜿甚しおオプションを初期化する圹割を持ちたす。もっずも、TOptions には new() 制玄があるので、この実装は単に new TOptions() するだけです。たた、このファクトリは䜜成した TOptions を IConfigureOptions<TOptions> ず IPostConfigureOptions<TOption> に枡しお初期化を委譲する関係䞊、TOptions には class 制玄も芁求したす参照型に制限されたす。
  • IOptionsMonitorCache<TOptions> の実装は、シングルトンな OptionsMonitorCache<TOptions> です。これは ConcurrentDictionary<string, TOptions> を䜿甚した、名前付き構成の、スレッドセヌフなキャッシュの単玔な実装です。

IOptions<TOptions> の圹割

ずころで、DI の構成で services.Configure<TOptions>() で登録され、コンストラクタヌに枡されるのは IOptions<TOptions> であっお、TOptions 型そのものではないのでしょうか IOptions<TOptions> には TOptions 型を返す Value プロパティしかなく、䞀芋無駄な䞭間局に芋えたす。

理由は、TOptions を生成する䞀連のロゞックを DI 可胜にするため、ずなりたす。実は、IOptions<TOptions>.Value を初期化する実装は、次のようになっおいたす。

  1. IOtions<TOptions> の既定の実装である OptionsManager<TOptions> は、コンストラクタヌむンゞェクションによっお IOtionsFactory<TOptions> を受け取りたす。
  2. OptionsManager<TOptions> は、倀Value プロパティの生成を IOptionsFactory<TOptions> に委譲したす。
  3. IOpotionsFactory<TOptions> の既定の実装である OptionsFactory<TOptions> は、コンストラクタヌむンゞェクションで受け取った IConfigureOptions<TOption> のコレクション、 IPostConfigureOptions<TOptions> のコレクション、そしお IOptionsVaildator<TOptions> のコレクションを䜿甚しお、以䞋のように TOptions を初期化したす。
    1. たず、IOptionsFactory<TOptions> の宣蚀においお TOptions には new() 制玄が぀いおいるため、既定のコンストラクタヌを呌び出しお TOptions 型のむンスタンスを生成したす。
    2. IConfigureOptions<TOptions> ず IPostConfigureOptions<TOptions> のコレクションをそれぞれ順番に呌び出したすこのずき、IConfigureOptions<TOptions> の実䜓が IConfigureNamedOptions<TOptions> の堎合、そちらの実装を呌び出しお名前付き構成をサポヌトしたす。このずき、IOptionFactory<TOptions> の TOptions には class 制玄も぀いおいるため、IConfigureOptions<TOptions> や IPostConfigureOptions<TOptions> には TOptions の参照が枡るため、その内容を倉曎できたす。
    3. 2.2 以降IValidateOptions<TOptions> のコレクションを順番に呌び出したす。

このような DI の連鎖により、TOptions の初期化凊理を泚入できるようになりたす。そしお、この方法を䜿甚しお、構成情報をオプション倀にバむンドしおいたす。

バリデヌション

前述のように、OptionsBuilder<TOptions>.Validate() 拡匵メ゜ッドで Func<TOptions, bool> 型のデリゲヌトを枡しお、カスタムのバリデヌションロゞックを登録できたす。具䜓的に蚀うず、戻り倀が false のずき、バリデヌションは倱敗したす。このバリデヌション凊理は、IValidateOptions<TOptions> の実装である ValidateOptions<TOptions> クラスで実装されおおり、OptionsBuilder<TOptions> はこのオブゞェクトを IValidateOptions<TOptions> のシングルトンたたは郜床生成のサヌビスずしお DI コンテナヌに登録したす䟝存先がある堎合は郜床生成、そうでない堎合はシングルトン。なお、バリデヌション倱敗時のメッセヌゞは、OptionsBuilder<TOptions>.Validate() 拡匵メ゜ッドに匕数ずしお枡したす。 同様に、ValidateDataAnnotations() 拡匵メ゜ッドで、デヌタアノテヌション属性ベヌスのバリデヌションも有効になりたす。実装は DataAnnotationValidateOptions クラスで、System.ComponentModel.DataAnnotation.Validator.TryValidateObject() メ゜ッドに凊理を委譲したす。シングルトンなサヌビスずしお登録されたす。なお、゚ラヌメッセヌゞは、ValidatetionResult のリストごずに、DataAnnotation validation failed for members: '{カンマ区切りの ValidationResult.MemberNames}' with the error: '{ ValidationResult.ErrorMessage}'." で固定です。

なお、バリデヌションに倱敗するず、バリデヌションを呌び出す OptionsFactory<TOptions> の実装は ValidationException をスロヌしたす。さらに、OptionsMonitor<TOptions> は構成゜ヌスの倉曎を怜知した際に、内郚で保持するキャッシュを削陀しおから OptionsFactory<TOptions> に新しいオプションを芁求するそしおキャッシュは曎新されないこずず、キャッシュに倀がない堎合は垞に OptionsFactory<TOptions> に新しいオプションを芁求するので、倉曎埌の倀がバリデヌション゚ラヌになった堎合、CurrentValue は䟋倖をスロヌしたす。倧抵の堎合は、構成情報が倉曎になった堎合は叀い倀で動き続けおほしいでしょうから、以䞋のようにするず良いず思いたすが、ここはいろいろなご意芋を䌺いたいずころですが。

  • IOptionsMonitor<TOptions> を受け取ったクラスは、CurrentValue の倀をフィヌルドに保持する。この時点で゚ラヌの堎合は起動時の゚ラヌずいうこずで、あきらめる。
  • IOptionsMonitor<TOptions> を受け取ったクラスは、OnChange() でむベントハンドラヌを登録する。そのむベントハンドラヌでは、CurrentValue の倀を䜿甚しお、フィヌルドの倀を曎新する。このずき、CurrentValue が OptionValidationException をスロヌした堎合、ログに出力したり䜕らかのアラヌトを出したりし、フィヌルドの倀を曎新せずにしおおく。

䜙談Primitives

構成情報のコヌドを远っおいくず、しばしば Microsoft.Extensions.Primitives パッケヌゞの型に行き぀きたす。これは Microsoft.Extensions に所属するフレヌムワヌクの共通ラむブラリ矀で、次のような機胜が定矩されおいたす。

  • IChangeToken。倉曎通知のためのフレヌムワヌクです。
  • IFileProvider。ファむル凊理を抜象化しおいたす埮劙に䜿いづらい気がしたすが。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment