Edited at
ASP.NETDay 12

asp.netにおけるc# scriptingの活用

More than 3 years have passed since last update.

この記事はASP.NET Advent Calendar 2015の12日目です。


はじめに

ASP.NET5 では、アプリケーションの設定にweb.configを固定で使うのではなく、開発者が設定元を選択することができる。

そのためのフレームワークがMicrosoft.Extensions.Configurationであり、ソースは下記URLに公開されている。

https://github.com/aspnet/Configuration

公式ドキュメントは以下

https://docs.asp.net/en/latest/fundamentals/configuration.html

ただし、部分的に実際のソースと異なっていたりするので、適宜読み替えが必要。

なお、2015/12月時点でリリースされているcoreclr-rc1をベースに解説していくので、

将来的に仕様変更が入るかもしれないが悪しからず。


使用方法

基本的には、以下の流れ


  1. IConfigurationBuilderで設定元を選択

  2. IConfigurationBuilder.Build()でIConfigurationRootを生成

  3. IConfigurationRootからGetSection及びGetChildrenで個々の設定の取り出し


コード例

using Microsoft.Extensions.Configuration;

IConfigurationRoot configRoot = new ConfigurationBuilder()
// ここでどの設定元を使うか指定
// 同じキーが設定されている場合、後にAddしたものが優先される
.AddEnvironmentVariables()
// 設定情報を確定
.Build();
// ルート直下に位置する設定を取得
IEnumerable<IConfiguration> configs = configRoot.GetChildren();
foreach(var config in configs)
{
// 設定を取り出して処理
Console.WriteLine($"key={config.Key},value={config.Value},path={config.Path}");
// config.GetChildren()できるので、再帰的に処理しても可
}
// サブセクションにアクセスしたい場合
IConfigurationSection section = configRoot.GetSection("sectionname");
foreach(var config in section.GetChildren())
{
// 設定を取り出して処理
}


ASP.NETチームが提供している実装

いずれも githubリポジトリにある。使用する場合は、対応するnugetパッケージを指定する。


  • 環境変数(Microsoft.Extensions.Configuration.EnvironmentVariables)

  • ini(Microsoft.Extensions.Configuration.Ini)

  • json(Microsoft.Extensions.Configuration.Json)

  • xml(Microsoft.Extensions.Configuration.Xml)


ASP.NET MVC 6における設定値の取り扱い


Dependency Injection

実際にIConfigurationで設定した値をASP.NET MVC6のController等の各コンポーネントに渡す手法としては、DIによるコンストラクタ経由の受け渡しが提供されている。

以下のようなコードで、各コントローラーのコンストラクタに引数を置くことで、値が渡され、メンバ関数として扱うことができる。

// ASP.NET MVC6テンプレートのStartupクラスの内部

// IConfigurationは、Startupクラスのプロパティとして存在している。
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IInterface>(x=>new ImplementationClass());
}

詳細ドキュメントはASP.NET公式の Dependency Injection にある。


IOption

取得したIConfigurationをDI設定してキーバリューとして扱ってもいいが、ASP.NET MVC 6では、IConfigurationからPOCOクラスへ

デシリアライズするIOptionという仕組みを用意している。

ソースは https://github.com/aspnet/Options で、

この仕組みを用意すれば、キーの存在確認等をすることなくC#のクラスとして扱うことができる。

public void ConfigureServices(IServiceCollection services)

{
// ConfigurationはStartupクラス内部のIConfigurationプロパティ
services.Configure<MyConfigurationClass>(Configuration);
}

詳しくはみていないが、IConfigurationを受け取るインターフェイスの他に、自分で作り方を定義できるパターンも使えるようなので、これで各Controllerに値を渡すことができる。

詳細ドキュメントは、ASP.NET公式のConfigurationのページを参照


設定元の拡張の典型的な実装

ASP.NETチーム提供のものでも十分機能的には問題ないと思われるが、より特殊なファイル形式や、

あるいはネットワーク越しに設定を取得したい場合もあるかもしれない。そのような場合は、

自分で設定方法を書いたクラスを作る必要がある。

以下では典型的な実装方法を書く。


大まかな流れ


  1. IConfigurationProviderを実装するか、ConfigurationProviderを継承する

  2. ConfigurationProviderを継承した場合は、public virtual void Load()をオーバーライドし、this.Dataに最終的に値を入れるように書く

  3. IConfigurationBuilderを第一引数と戻り値にした拡張メソッドを書き、その中でIConfigurationBuilder.Addで自分のプロバイダーを追加する(最終的にはこれを外部から呼び出す)

今回はIConfigurationProviderを実装する方法の方は確認していないが、ConfigurationProviderの実装を見てみれば、どのように実装すればいいかはある程度わかると思う。

また、ファイルを読み込み対象とする場合は、boolでファイルが存在しない場合にスキップするかどうかを選択できるといいかもしれない。


this.Dataに入れる内容

this.DataはIDictionary<string,string>となっているが、ConfigurationProviderの実装を見てみるとキー名の大文字小文字は区別しないような想定になっているので、

new Dictionary<string,string>(StringComparer.OrdinalIgnoreCase)して値を詰めていくのがいいと思われる。

また、GetSectionで取得できるように階層構造を作りたい場合は、

キーをMicrosoft.Extensions.Configuration.Constants.KeyDelimiter(=':')で

区切った要素を追加すれば、それが階層構造になる。

例:


MyConfigurationProvider.cs

class MyConfigurationProvider : ConfigurationProvider

{
public override void Load()
{
this.Data = new Dictionary<string,string>()
{
{"a:b","c"},
};
}
}


MyProgram.cs

// IConfigurationRoot configRoot;

var sub = configRoot.GetSection("a");
var config = sub.Get("b");
Console.WriteLine($"{config.Key},{config.Value}");
// b,c


実装例(C#からの設定読み込み)

Roslyn C# Scriptingも正式版(1.1.1)になったので、.csxファイルから設定を読み込むプロバイダーを作成してみた。

リポジトリは以下。

https://github.com/itn3000/Sample.CsConfig


利点


  1. コメントが使える

  2. 定数が使える(サンプルでは使ってないけど)

  3. (似非)型が共有できる(IOptionsと組み合わせ)

シリアライズに関しては、基本型とそれのみを含むサブクラスじゃないとサンプルコードでは恐らく死ぬので注意。

3については、ASP.NET MVC 6 を使う場合は、IOptionsとの連携となる。

この場合、POCOクラスじゃないとデシリアライズで死ぬと思われる。

似非と書いているのは、今回の方式ではアプリケーション側の.csを単体のcsxとして読み込み、その中で値を詰めているので、

厳密にいえば異なる型になるため。


欠点


  1. namespaceが使えない(usingは使える)

  2. 外部要素(クラス等)を使う場合に制限がある(ホストアプリケーションとの型共有に影響)

  3. 大掛かりになる?

  4. 補完が効かない時がある

1番目の制限はC# Scriptingの仕様に由来。

2番目の制限は割と大きくて、自分のプロジェクトのクラスでも、明示的にAddReferenceするか、

ソースを追加しないと使えないので、その辺りは注意する必要がある。

nugetで入れたアセンブリは、dnxcore50ではGetTypeInfo()から追加可能みたいだけど、プロジェクトごとのアセンブリの追加では

System.NotSupportedException: Can't create a metadata reference to an assembly without location.が出る。Locationがないという意味の例外なので、

この辺りは実体のファイル(DLL等)があるかないかの違いかもしれない。

もしかしたら回避方法があるかもしれないが、執筆時点では見つからなかった。

CSファイルの読み込みパス等にも注意が必要で、.WithSourceResolverや.WithMetadataResolverの設定がScriptOptionsに必要。

参考: http://www.baku-dreameater.net/archives/7311

3番目は、Microsoft.CodeAnalysis.Scriptingの依存パッケージが多いことに由来する。

また、このせいで.NET Framework 4.6じゃないと動かないので、frameworkに"dnx451"があると、clr環境で動かしたときにエラーになる。

動かすなら"dnx46"あるいは"dnxcore50"にすること。

4番目は、サンプルコードはVisutal Studio Codeで書いていたんだけど、新規追加直後は認識されなくて、プロジェクトの読み込み直し、あるいはdnx実行後等に補完が効き始めたりするなどしていた場合があった。

ただ、厳密な再現条件というのは不明なので、自分のやり方が悪いだけかもしれない。


調べてみて思った事

当初はc#scriptingの活用法を考えて、そのための記事を書こうと思ってたけど、前提となるMicrosoft.Extensions.Configuration部分の記述が多くなってしまったので、最終的にこのような記事になった。

web.configしかなかった頃に比べ、設定元の選択が選べるということで、かなり扱いやすくなっていると思う。

C# Scriptingとの組み合わせに関しては、使い込もうと思うと制限があるかもしれないけど、

ほとんど起動時一発目しか読みにいかないような処理なので、適当な所で妥協して使うのがいいかなと思った。