自分の整理、理解も兼ねて。
Microsoft.Extensions.Configuration とは
「設定(=ソースコードにベタ書きせず外部に変更可能な項目を用意すること)」を良い感じに共通化したライブラリ。
Iniファイル、JSONファイル(もちろんappsettings.jsonも含む)、環境変数、コマンドライン引数など、いろんなものを「設定」として扱うことができる。
今回は主にJSON(appsettings.json)を扱う。
設定の読み込み
ConfigurationBuilderを使う。
IConfigurationRoot configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", false)
.Build();
ConfigurationBuilderのインスタンスに対してAddXXXXX()でツラツラと設定のソースとなるファイルなどを書き連ねていく。
設定のソース毎に「Microsoft.Extensions.Configuration.Json」とか「Microsoft.Extensions.Configuration.EnvironmentVariables」とかNuGetを追加する。
上記のソース例だとappsettings.jsonを必須として指定している。(2つ目の引数はbool optional)
実行ファイル(要検証)と同じディレクトリにappsettings.jsonファイルが無いとエラーになる。
最後に.Build()してやると実際に読み込みとパースを始める。
ファイルの不存在とかフォーマット違反とかはここでエラーになる。
ConfigurationBuilder.Build()の戻り値はIConfigurationRoot型になる。
これはIConfigurationインターフェースの派生で、M.E.Configurationの拡張メソッドとかは大体IConfigurationから生えている。
設定の読み出し(使用)
IConfigurationから実際の設定を読むには、.Get<T>()メソッドを使う。(GetValue<T>()でも良いけどね)
Tは読み出したい型。プリミティブでもClassでも良い。
ただし、appsettings.jsonに対して.Get<string>()とかやってもあんまり実用的ではない。
(ヒマな人は試してみると面白いかも。)
実用的には.Bind()拡張メソッドを使う。
1番目の引数はKey文字列、2つ目には「Key配下のConfigurationをプロパティーにBindするインスタンス」を指定する。
例えば、こんなappsettings.jsonがあったとする。
{
"Hoge":{
"Name": "ほへへへへ",
"Prop": {
"Age": 18,
"IsEnabled": true,
}
}
}
で、受ける側はこんなClassを用意していたとする。
public class HogeConfig {
public HogeConfig() {}
public string Name { get; set; } = string.Empty;
public Props Prop { get; set; } = new Props();
}
public class Props {
public Props() {}
public int Age { get; set; } = 10;
public bool IsEnabled { get; set; } = false;
}
で、こんなことをしたとする。
IConfigurationRoot configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", false)
.Build();
HogeConfig hogeConfig = new HogeConfig();
configuration.Bind("Hoge", hogeConfig);
その結果、hogeConfigにはappsettings.jsonの中身が良い感じでおさまる。(Props型の中身も含めて。これだけでもConfigurationを使う価値は十分にある!)
Bind()のKeyにはJSONのディクショナリの名前を入れるところがポイント。
(そうしないとappsettings.jsonの最上層のHogeというプロパティー名をHogeConfig型の中から探すので全滅する。)
appsettings.jsonの1ファイルの中にM.E.Loggingの設定と、Kestrelの設定と、自分の設定を仲良く同居させることができる。
同じ型の設定を名前を変えて複数書くこともできる。
大体のPrimitiveとstring、配列、Classは良い感じで解釈してくれる。
(唯一自分が面倒だと感じたのはIpAddress型を理解してくれないところ…。この場合は一旦Classの方でstring型のプロパティーにしておく必要がある。そうでないと「0.0.0.0」に解釈されてしまう。)
jsonファイルの項目名の大文字小文字とか、プロパティーの大文字小文字とかはどうなるのかよく覚えていない。
どっかに「大文字小文字は区別しない」と書いてあった気がする。
DipendencyInjection(ServiceCollection/ServiceProvider)と合わせて使う場合は、
services.AddSingleton<IConfiguration>(configuration);
services.Configure<HogeConfig>(configuration.GetSection("Hoge"));
なんて書いておくと、ServiceCollection(Provider)にHogeConfigのインスタンスを登録して、その中身をConfigurationからよきに計らって入れておいてくれる。
(ASP.NetとかM.E.Hostingなどを使っている場合はそもそも1行目はHostBuilder()の中などで既に行われていたりする。かつ、IConfigurationRootをそのまま登録しておくと「IConfigurationが登録されていないよ!」的な旨のイケズなエラーメッセージに遭遇することになる。)
で、sp.GetRequiredService<HogeConfig>()とかやると設定済みのHogeConfigのインスタンスが受け取れる。
ごめんなさい、ウソつきました。
service.Configure<T>()はM.E.Options.ConfigureExtensionsに入っている拡張メソッドなので、SPに登録されるのでOptions側のインスタンス(IOptions<T>)です。
ささっと見た限りでは、Bind()後のConfigurationをSPにさっくり登録する拡張メソッドが見つかりませんでした。
ご存じの方はコメントで教えていただけると助かります…。
後で説明するが、ここでの値はBind()した時点で決定されています。(変更されない)
DependencyInjectionで他のClassでコンストラクターインジェクションして使用する場合はコンストラクタ引数に設定Class(ここではHogeConfig)そのものを使用します。
public class UseHogeConfig
{
private readonly HogeConfig HogeConfig;
public UseHogeConfig(HogeConfig config)
{
HogeConfig = config; // コンストラクタインジェクションされる
Debug.WriteLine($"HogeConfig.Name={HogeConfig.Name}");
// → HogeConfig.Name=ほへへへへ
}
}
Microsoft.Extensions.Options とは
Configurationを発展されたライブラリ(だと個人的には考えています)。
設定をよしなに使うことを目的としているところは同じですが、設定が変更された場合に追随できるところが大きく違います。
設定の読み込み
大体においてConfigurationと組み合わせて使います。(単独で使う方法もあるんだとは思いますが使ったことない)
設定ファイルなどの読み込みや解釈はConfigurationが行います。
Configurationとの違いは、設定ソースが変更された場合に追随させたりイベントを受け取ったりできるところです。
Optionsとしての設定はAddOptions()とAddOptions<T>()メソッドを使います。
services.AddOptions();
services.AddOptions<HogeConfig>()
.BindConfiguration("Hoge")
.ValidateOnStart();
上記は前段のConfigurationのサンプルの後であることが前提です。
ServiceCollection(ServiceProvider)にIConfigrationRootが登録されていることが前提です。設定されていないとエラーになります。
ジェネリクス無しのAddOptions()はOptionsを使うための(全体の?)準備。
ジェエリクス有りのAddOptions<T>()が実際の設定Class(HogeConfig)にConfigurationからの中身をセットしてServiceCollectionに登録します。
(ちゃんと検証していませんが、AddOptions()を実行しないと後述する設定変更の自動反映などが効かないような気がします。しらんけど。)
読み出し(使用)
Optionsから設定を読み出すには、設定を使用するクラスのコンストラクタインジェクションでIOptions<T>やIOptionsMonitor<T>を使い、そこから読み出します。
IOptions<T>やIOptionsMonitor<T>はTとは何の関係もありません。(派生でも継承でもない。)
public class UseHogeOptionsConfig
{
private readonly IOptions<HogeConfig> HogeOptions;
public UseHogeOptionsConfig(IOptions<HogeConfig> config)
{
HogeOptions = config; // コンストラクタインジェクションされる
HogeConfig cfg = HogeOptions.Value;
Debug.WriteLine($"HogeConfig.Name={cfg.Name}");
// → HogeConfig.Name=ほへへへへ
}
}
上記のサンプルではIOptions<HogeConfig>の.Valueプロパティーを取りだして設定として使用しています。IOptions<HogeConfig>にはHogeConfigクラスのプロパティーは生えていません。
正直、IOptions<T>を使う限りにおいては(私は)Tを直接DIして使うのと違いが判りません。(面白くない)
一方、IOptionsMonitor<T>を使うと設定のソースが変更されたときに最新の状態を使えたり、設定自体が変更されたことを知ることができます。
public class UseHogeOptionsMonitorConfig
{
private readonly IOptionsMonitor<HogeConfig> HogeOptions;
public UseHogeOptionsMonitorConfig(IOptionsMonitor<HogeConfig> config)
{
HogeOptions = config; // コンストラクタインジェクションされる
HogeConfig cfg = HogeOptions.CurrentValue;
Debug.WriteLine($"HogeConfig.Name={cfg.Name}");
// → HogeConfig.Name=ほへへへへ
var monitor = HogeConfig.OnChange(x => {
HogeConfig cfg = HogeOptions.CurrentValue;
Debug.WriteLine($"HogeConfig is changed !! .Name={cfg.Name}");
});
}
}
まず、.Valueではなく.CurrentValueを使います。
さらにIOptionsMonitor<T>にはOnChange()というメソッドがあります。設定が変更された場合はここのメソッドに設定したActionが実行されます。
このActionの中で設定変更時にサーバの設定を変えて再起動するとか、そういうことができます。
OnChange()の戻り値はIDisposableなので、使い終わった時(=IOptionsMonitorを使っているClassがDispose()されるときなど)にDisposeする必要があります。
この例ではコンストラクタ内のローカル変数に入れていますが、本当はクラスのフィールドに入れてやり、適宜Disposeしてあげるのが正しいです。
OnChange()を使っていなくても.CurrentValueには呼び出し時点での最新の設定が反映されています。参照頻度が低いのであれば都度.CurrentValueを見て何か処理をしても(まぁ)いいでしょう。(負荷とかどうなるか知らんけど。)
M.E.Configuration と M.E.Options の違い
注意しなくてはならないのは、ConfigrationでのBind<HogeConfig>(~)やConfigure<HogeConfig>(~)とOptionsでのAddOptions<HogeConfig>(~)は互いに全く関係ないという事です。
ServiceProviderに設定されるオブジェクトも違いますし、例えばAddOptions<T>()したからと言ってTがSPに自動的に登録されることもありません。
これはTを使っていたClassを後からIOptions(Monitor)<T>に対応させた場合に他の場所でTがコンストラクタインジェクションされることを期待していたりすると意外とハマって無駄に悩むことになります。
逆もまたしかりです。
あと、WebApplicationBuilderやHostBuilderを使っている場合は良いのですが、自前でConfigrationやOptionsやServiceProviderを用意している場合はそれぞれの関りをうっかり忘れてアプリ起動時に(不親切な)エラーメッセージに悩んだりします。
(UnitTestとかね…。)
ConfigurationはServiceProviderと関係なしに使うことができますが、OptionsはSPにもConfigurationにも依存しますので注意が必要です。
あと、IOptionsMonitor<T>.OnChange()の戻り値を取っておかないとアプリ終了時とかの変なタイミングでわけのわからないエラーが出て悩むことになります。ちゃんとClassのフィールドとして取っておいて、最後にDisposeしましょう。
さらに、設定ファイルのフォーマットエラーや設定の意味的なミスについて、ConfigurationはBind()実行時にエラーになりますが、OptionsはIOptions<T>やIOptionsMonitor<T>をインジェクションする時点でエラーになります。アプリ初期化時ではないので注意が必要です。
最後に
ここでは説明していませんが、Configurationの最大の利点は設定のValidationができることです。
IOptionsMonitor<T>での設定エラーの検出もAddOption<T>()の後に.ValidateOnStart()を付けてやることで初期化時にValidationしてくれます。
ValidationはEntity Framework CoreでおなじみのSystem.ComponentModel.DataAnnotationsパッケージの属性(Attribute)で指定する方式やValidatorを設定して確認する方式が使えます。
[Require]の意味が微妙に異なるところとかを除けばまぁ同じように使えます。
(EFCoreの[Require]はDBカラムの「not null」を指定する為に使いますが、Configurationでは「設定(ファイル)に必ず書いてあること」を指定する為に使います。クラス定義でnullではなくデフォルト値が設定されていても設定ファイルに書かれていないと問答無用でエラーになります。)
途中にも書きましたが、唯一気に入らないのはConfigurationのValidationで「IPAddressのValidationや設定」ができないことです。
厳密にはそのあたりはValidatorで書いてあげれば対処できなくはないのですが、サーバを書こうとするときにイラッとします。
(URLやEmailのValidationは標準であるのにねー。)
設定Classを自分で書ける場合はプロパティーの型をIPAddressではなく一旦stringにしてやって、使うときに変換することで回避できます。
ただ、外部のライブラリを使っていてその設定をConfigration/Optionsから使おうとしたときに~OptionsクラスにIPAddress型が使われていると軽く絶望を味わうことになります。
(MQTTnetとか、Kestrelとか、Web APIとか…)
何はともあれ、「.NETらしい設定様式」をコーディングする時にはConfigration/Optionsは非常に役に立つのではないでしょうか。