ASP.NET Core アプリのアプリケーション構成ASP.NET Core アプリは、標準のプロジェクトテンプレートで作成すると、プロジェクトのフォルダにある appSettings.json というファイルを読みこんで、アプリケーション構成として使用するようにできている。
この appSettings.json にアプリケーション固有の半固定パラメータなどの設定値を JSON 形式で記述しておき、アプリ中からその設定値を読み取って利用する、というものだ。
詳しくは下記を参照するとよいだろう。
例えば下記のような appSettings.json を用意したとして、
{
"SiteSettings": {
"Title": "Hello World",
"TimeZone": {
"Name": "Tokyo Standard Time",
"Offset": 9
},
"Tags": [
{ "Text": "Foo", "Priority": 0.7 },
{ "Text": "Bar", "Priority": 0.3 }
]
}
}
ASP.NET Core アプリの実装コード (今回は C#) で、下記のように、上記 appSettings.json の JSON 構造にマッピングされるクラスを用意して、
public class SiteSettings {
public string Title { get; set; }
public TimeZoneSettings TimeZone { get; set; }
public class TimeZoneSettings {
public string Name { get; set; }
public int Offset { get; set; }
}
public TagSettings[] Tags { get; set; }
public class TagSettings {
public string Text { get; set; }
public double Priority { get; set; }
}
}
ASP.NET Core アプリの Startup クラスにて、下記のように、appSettings.json ファイル中の SiteSettings ノード以下を DI コンテナへサービス登録することで、
public class Startup
{
public IConfiguration Configuration { get; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.Configure<SiteSettings>(Configuration.GetSection("SiteSettings"));
...
`IOptions<SiteSettings>` 型のインスタンスを DI 機構によって入手できるようになる。
この `IOptions<SiteSettings>` オブジェクトの各プロパティを参照すると、appSettings.json の記載内容が SiteSettings オブジェクトにマッピングされて読み取られ、参照可能となっている寸法だ。
// 変数 settings は、DI 機構から入手した IOptions<SiteSettings> オブジェクトへの参照
settings.Value.Title; // <- "Hello World"
settings.Value.TimeZone.Name; // <- "Tokyo Standard Time"
settings.Value.TimeZone.Offset; // <- 9
settings.Value.Tags[0].Text; // <- "Foo"
settings.Value.Tags[1].Text; // <- "Bar"
アプリケーション構成の "上書き" この appSettings.json に記載されている設定値だが、いくつかの方法で "上書き" が可能だ。
ひとつは appSettings.Develop.json に記述する方法。
開発環境での実行時は、appSettings.json の設定値に対して、appSettings.Develop.json に記述した設定値で上書きされたように、アプリ側では設定値を読み取るようにできているのだ。
この仕組みにより、appSettings.json には既定値を、appSettings.Develop.json には各開発者固有の環境依存の設定値など(例えばデータベースの接続先設定値とか)を記載しておいたりして、アプリの動作を調整できる。
例えば先の例だと、appSettings.Develop.json に以下のように記述しておくと、
{
"SiteSettings": {
"TimeZone": {
"Name": "Asia/Tokyo",
}
}
}
アプリ側の読み取り結果は下記のとおりとなる。
settings.Value.Title; // <- "Hello World"
settings.Value.TimeZone.Name; // <- "Asia/Tokyo" ... 上書きされた!
settings.Value.TimeZone.Offset; // <- 9
settings.Value.Tags[0].Text; // <- "Foo"
settings.Value.Tags[1].Text; // <- "Bar"
なお、appSettings.Develop.json の "Develop" のファイル名部分は、実は環境変数 ASPNETCORE_ENVIRONMENT の値である。
なので、環境変数 ASPNETCORE_ENVIRONMENT に "FooBar" を設定すれば、appSettings.FooBar.json が上書き用に読み込まれる。
アプリケーション構成のセクション名は英字の大小が区別されない話は逸れるが、面白い(?)ことに、appSettings.Develop.json で appSettings.json の設定値を上書きする際、セクション名 (ノード名、プロパティ名) の英字の大文字と小文字は一致しなくても構わない。
アプリケーション構成のセクション名は Case Insensitive として動作するのだ。
なので、appSettings.json に、英字の大小違いの同一セクションを設けると(例えば下記)、JSON としては間違いではないのだが、その ASP.NET Core アプリは起動時にクラッシュしてしまう。
{
"SiteSettings": {
"title": "hello world", // <- セクション名の先頭が小文字
"Title": "Hello World", // <- セクション名の先頭が大文字
IIS 経由で実行した場合は
「HTTP Error 502.5 - Process Failure - The application process failed to start」
として現れるし、「dotnet run」で実行すると、
「Unhandled Exception: System.FormatException: A duplicate key 'SiteSettings:Title' was found.」
というように、重複キー発生である例外メッセージがコンソール出力から読み取れる。
環境変数で "上書き"アプリケーション構成を上書きするもうひとつの方法は、環境変数で "上書き" する方法である。
appSettings.json 中の各セクション (ノード、プロパティ) 名を、コロン (:) 区切りで連結した名前で環境変数を定義すると、その環境変数の値がアプリケーション構成として読み取れるのだ。
言い換えると、C# や JavaScript 上、ドット (.) で区切るところをコロン区切りに置き換えた名前、とも言える。
先の例でいくと、例えば環境変数 "SiteSettings:TimeZone:Offset" に "8" と設定してから実行すると、アプリでの読み取り結果は下記のとおりとなる。
settings.Value.Title; // <- "Hello World"
settings.Value.TimeZone.Name; // <- "Tokyo Standard Time"
settings.Value.TimeZone.Offset; // <- 8 ... 9 から 8 に上書きされた!
settings.Value.Tags[0].Text; // <- "Foo"
settings.Value.Tags[1].Text; // <- "Bar"
Windows OS 上で Visual Studio を用いて開発・実行している場合は、Visual Studio の画面上、プロジェクトのプロパティ画面から環境変数を設定することもできる。
![d0079457_23144057.png]()
このやり方だと、環境変数の設定を変更して保存 (Ctrl +S) すると、Visual Studio が自動で ASP.NET Core アプリのプロセスを再起動してくれ、アプリケーション構成が再読み取りされて、設定内容が即反映されるのだ。
appSettings.json 及び appSettings.Develop.json を編集・保存した場合は、そのようなプロセス再起動をはじめ、自動でアプリケーション構成を再読み取りされることはないようである。
(どこかで、appSettings.Develop.json が変更されたら再読み取りするオプション指定を見かけたような気もするのだが。)
ちなみに、こうして Visual Studio 上から設定した環境変数設定は、プロジェクトのフォルダ以下、Properties フォルダ内の launchSettings.json 内に書き込まれる。
「dotnet run」で実行する際も、この launchSettings.json の内容は読み取られて使われる。
なので、launchSettings.json に環境変数を設定しておいて「dotnet run」で実行すると、その環境変数設定が反映されて実行される。
ご参考までに。
また、これも至極当然のことではあるが、こうして環境変数で上書きする際も、セクション名 (環境変数名) の英字の大小は区別されない。
Microsoft Azure の App Service 設定での "上書き"
実は、Microsoft Azure の App Service 上に Web アプリを配置したときに、Azure ポータルから「アプリケーション構成」で設定する値も、アプリ側には環境変数として現れる。
なので、この仕組みで、ASP.NET Core アプリのアプリケーション構成を Azure ポータルから上書き設定することができるのだ。
その際のセクション名の指定方法は、上記のとおりコロン区切りでセクション名を連結した名前を、Azure ポータル「アプリケーション構成」の欄の "APP SETTING NAME" のところに指定してやれば良い。
"上書き" というよりは "合成" と言ったほうがイメージあってる?
これら "上書き" の動作だが、優先順位としては次のとおり。
環境変数 > appSettings.Develop.json > appSettings.json
上記3者で同じセクション名で異なる値をそれぞれ設定していた場合、環境変数による設定が勝つ。
また、"上書き" とは言ったが、appSettings.json 内に記述のないセクション名のアプリケーション構成を、環境変数で設定してあれば、アプリ側ではちゃんと読み取れる。
例えば appSettings.json に SiteSettings.Title がなくても、
"SiteSettings": {
"TimeZone": { // <- "Title" プロパティがない!
...
環境変数 "SiteSettings:Title" に "HELLO!" が設定されていれば、アプリ側ではちゃんと読み取れる。
settings.Value.Title; // <- "HELLO!" ... 環境変数での設定が見えている!
なお、上記のような例を見ると、appSettings.json に存在しないセクションなので、存在しないものに対して "上書き" という言い方には違和感を覚えるかもしれない。
上書きというよりかは、Key-Value の辞書のツリーを "合成" (マージ) していくようなイメージのほうが理解しやすいかもしれない。
よくよく考えたら、環境変数を読むのに使えたり。ここまでを振り返ってよくよく突き詰めて考えてくと、実は ASP.NET Core のアプリケーション構成の仕組みで、環境変数がまるっと取得できてしまったりもするのだ。
ここまで紹介してきた例では、Startup にて、"Configuration.GetSection("SiteSettings")" と設定していたので、すべての環境変数は見えず、"SiteSettings:~" で始まる名前の環境変数だけ見えていた。
これを改め、単に "Configuration" オブジェクトそのものを渡してバインドすれば、すべての環境変数値がバインド可能だ。
また、ASP.NET Core アプリケーション構成の仕組みは、必ずしも静的型付けしなくては使えないわけではなく、Configuration オブジェクトを参照し、その GetValue などのメソッドを使って、文字列でセクション名 (= 環境変数名) を指定して取得することもできる (下記など参考に)。
本来の用途ではないが、ASP.NET Core のアプリケーション構成の仕組みを介して、プロセスの環境変数をすべて参照することも可能だ。
ところで、コレクションって何にバインドできる?ところで、今回の例にある、"Tags" セクションは、JSONN 上、配列になっており、これのバインド先の SiteSettings クラスでも配列としている。
public class SiteSettings {
...
public TagSettings[] Tags ... // <- TagSettings 型の "配列"
...
試したところ、実はここ、配列じゃなくても大丈夫だった。
"List<T>" でも "IEnumerable<T>" でも "ICollection<T>" でもバインドできた。
public class SiteSettings {
...
public IEnumerable<TagSettings> Tags ... // <- これでも OK!
...
果たして "T[]"、"List<T>"、"IEnumerable<T>"、"ICollection<T>"、etc. のいずれでバインドするのが最適なのか、自分にははっきりとは判断つきかねる。
ただ、どちらかといえばいちばん書き換えできなさそうな "IEnumerable<T>" がいいのかなぁ、と漠然と考えている。
環境変数からコレクションの設定を上書きするには?さて今回の例の "Tags" セクション内の配列構成を、環境変数で上書きするには、どのような変数名を指定すればよいのだろう?
最初は、"SiteSettings:Tags[0]:Text" というような、C# や JavaScript などでよくあるインデクサ表記を環境変数名とすることで上書きできるのかと考えた。
が、試したところこれは間違い。
正解は "SiteSettings:Tags:0:Text" だ。
これは "JavaScrit 的な考え方" に慣れてるとわかりやすいと思う。
すなわち、配列の要素を、例えば0番目の要素であれば「Tags オブジェクトの、"0" という名前のプロパティ」というように捉えるのだ。
今回の例の、上書き前のオリジナルの appSettings.json の構成内容だと、Tags プロパティの内容は下記のとおりとなっている。
settings.Value.Tags[0].Text; // <- "Foo"
settings.Value.Tags[0].Priority; // <- 0.7
settings.Value.Tags[1].Text; // <- "Bar"
settings.Value.Tags[1].Priority; // <- 0.3
ここで環境変数 "SiteSettings:Tags:1:Text" に "Hoge" を設定すると、下記のとおり上書きされる。
settings.Value.Tags[0].Text; // <- "Foo"
settings.Value.Tags[0].Priority; // <- 0.7
settings.Value.Tags[1].Text; // <- "Hoge" ... "Bar" から上書きされた!
settings.Value.Tags[1].Priority; // <- 0.3 ... ここはそのまま!
上書き前の配列要素数を超えたら...?
さて、では今回の例において、環境変数 "SiteSettings:Tags:999:Text" に "Hoge" を設定したらどうなるのか!?
アプリ側で読み取った結果はこうなった。
settings.Value.Tags[0].Text; // <- "Foo"
settings.Value.Tags[0].Priority; // <- 0.7
settings.Value.Tags[1].Text; // <- "Bar"
settings.Value.Tags[1].Priority; // <- 0.3
// Tags[2] が増えた!
settings.Value.Tags[2].Text; // <- "Hoge"
settings.Value.Tags[2].Priority; // <- 0.0
一瞬戸惑ってしまったが、改めて JavaScript 脳になって考えてみると、何のことはない、環境変数名の "999" は「配列の添え字」ではなく、プロパティ名なのだ。
どうやら、ASP.NET Core のアプリケーション構成の仕組みは、バインド先の型がコレクションである場合、バインド元の値の "プロパティ" を列挙して、その値を順繰り、バインド先のコレクションの要素に編成しているようである。
配列、じゃなくて、結局は Key-Value の辞書であるなので、数字だけじゃなくて、任意の識別子を当てはめられる。
例えば環境変数名を、"SiteSettings:Tags:999:Text" ではなく、"SiteSettings:Tags:FizzBuzz:Text" としても、結果は同じになるのだ。
また、環境変数からの上書きというのではなく、appSettings.json の記述そのものを辞書形式で記述することもできる。
"SiteSettings": {
...
"Tags": [
{ "Text": "Foo", "Priority": 0.7 },
{ "Text": "Bar", "Priority": 0.3 }
]
...
は、アプリケーション構成的観点では以下のように記述してるのと同義と言える。
実際、アプリでの読み取り結果も変らなかった。
"SiteSettings": {
...
"Tags": {
"0": { "Text": "Foo", "Priority": 0.7 },
"1": { "Text": "Bar", "Priority": 0.3 }
}
...
さらには、プロパティ名 (Key) は数字である必要すらない。
下記でも TagSetting[] にバインドされ、読み取り結果は変わりない。
"SiteSettings": {
...
"Tags": {
"Alpha": { "Text": "Foo", "Priority": 0.7 },
"Omega": { "Text": "Bar", "Priority": 0.3 }
}
...
ちなみに、上記のような感じで Key-Value 辞書オブジェクトをコレクションにバインドした場合、その要素の追加順序は、プロパティ名 (Key) のオーダーとなるようだ。
削除はできなさそうなお、このような仕掛けであるようなので、appSettings.json で設定した配列構成要素を、appSettings.Develop.json や環境変数から "削除" することはできなさそうだ。
例えば今回の例において、appSettings.Develop.json を以下のとおり記述し、Tags の設定内容を要素数 0 件に潰そうとしたとする。
"SiteSettings": {
...
"Tags": [] // オリジナルの Tags 構成値を0件にしてやる!
...
しかしこれは意図したとおりには動作しない。
配列ではなく、"0" や "1" といった配列序数を名前としたプロパティを持つオブジェクトを "上書き" するのだ、と JavaScript 脳で考える必要がある。
JavaScript で言うならば、オリジナルの Tags オブジェクト に対し、空のオブジェクトで Object.assign するようなものだ。
var tags = {"0":{...}, "1":{...}};
var tagsDevlop = {}; // "Tags":[] は空オブジェクトと同義
// 空オブジェクトを assign しても、tags は何の変化もなし!
Object.assign(tags, tagsDevelop);
結果として、上記 appSettings.Develop.json は、何の効果ももたらさないこととなる。
そう、JavaScript の Object.assign を知っているならば、ASP.NET Core アプリケーション構成の "上書き" の仕組みは、Object.assign であると考えると間違いがないだろう。
まとめ以上、ASP.NET Core のアプリケーション構成の上書き (というか、"合成") の振る舞いについて色々試した結果となる。
ざっくりおさらいすると下記のとおり。
英字の大文字・小文字は区別されない。
appSettings.Develope.json より環境変数のほうが勝つ。
環境変数ではセクション名をコロンで区切って設定。
Visual Studio での環境変数設定は Properties\launchSettings.json ファイルに保存される。
コレクションのバインド先は "T[]", "List<T>", "IEnumerable<T>", "ICollection<T>" のいずれでも OK。環境変数でコレクションの要素を上書きするには、序数をプロパティ名として設定。
コレクションもその正体は、"0" や "1" といった序数をプロパティ名とした JavaScript オブジェクト (辞書) であるだけ。
なので、序数じゃない任意の識別子も指定できちゃう - その場合、プロパティ名 (Key) のオーダーでコレクションが編成される。
結局、JavaScript の Object.assign() で重ね書きしてる感じ。
なので、appsettings.json に設定済みの要素を、削除することはできないと思われる。
あと、最後になってしまったが補足しておくと、ここまでのアプリケーション構成の仕組みは ASP.NET Core アプリに限らず、Microsoft.Extensions.Options でアプリケーション構成を扱う .NET アプリ全てに適用される。
今回の投稿はかなりの大作となってしまった。
これでアプリケーション構成の "上書き" で迷うことはなくなった...と思いたい。