CbOtionsCore extensions for ConfigurationBuilder() Util with in Azure Functions 2.0 / .NET Core 2.0

この NuGet モジュールは、Azure Functions 2.0 runtime / .NET Core 2.0 環境下で、デバッグや xUnit を利用した単体テストなどの実行環境にあわせてユーザーが指定する設定値をどのようにプログラムに渡すか、という課題に対して、 ASP.NET Core で定評がある ConfigurationBuilder() を利用して解決する、幾つかのアプローチのうちの一つの実装例です。

NuGet is here.

Project site on GitHub is here.

TL;DR;

ASP.NET Core で導入された ConfigurationBuilder() はとても便利で使いやすい実装です。これを、Azure Functions 環境下でも便利に使いましょうよ、というもの。もちろん、Azure Functions の関数でベタベタに書けば、いかようにも書けるのですが、折角なので、Auzre Functions の Binding っぽく、各種トリガ関数のエントリポイント・メソッドにパラメータ引数として渡すように書いてみたよ、ASP.NET Core での DIっぽい感じで受け取れるようにしてみましたよ、という実装です。

結果として、ASP.NET Core で業務ロジックを別プロジェクトで作成しているような構成のソリューションにおいて、Azure Functions でも単体テストの設計がしやすくなりました。

というか、Azure Functions って、テストしづらいな、と感じてます。サービスの設計側で想定してるテスト方法が、コーディング・ミニマイズなAzure Portal での完結を優先的に想定してるようで、ASP.NET Core / Visual Studio 2017 を使って、業務システムを開発してる場合の CIのプロセスとはいまいち合致してなくて「帯に短し襷に長し」だと感じてます。

で、CIでもVS上でも単体テストを回しやすくするために、設定値を切り替える振る舞いの利便性を改善してみたよ、自分が欲しかったので。ついでに汎用性ある感じで Azure Functions 2.0 の SDK 使って拡張っぽくしてみたよ、という内容です。

使い方は README 見てください。 https://github.com/arichika/CbOptionsCore/blob/master/README.md

この投稿では、そのように実装したくなった意図・背景を中心に書いています。

この実装は何が “嬉しい” のか

この実装は、以下のような方々にとってハッピーな体験を提供する可能性があります。

  • すでに ASP.NET Core / ConfigurationBuilder() を利用し、強い型付けによる設定値の管理を実践している。
  • すでに .NET Core / .NET Standard で業務ロジック専用のアセンブリを作成したことがあり、そのモジュールの設計は、開発時、単体テスト時、本番運用時に、DBの接続文字列や参照先ストレージへの接続などの設定値を適切に指定することで、期待した振る舞いをするようになっている。
  • Visual Studio 2017 を利用している。

このような方々が、Azure Functions 2.0 向けに動作するプログラムを開発しようと思いたち、 Visual Studio 2017 で Functions のテンプレート・プロジェクトから何かを作成しようとして、業務アセンブリ用のプロジェクトを別に切り出した際に、単体テストの設計で「う」と困るケースを軽減します。

  • 自分で Azure Functions のトリガやパラメータ引数を作成する際の、ミニマムな実装のサンプルが知りたい。

こんな方にもどうぞ。

この実装は何を “改善しない” のか

今回の実装では、以下の項目について実装もなければ、改善もしません。

  • パラメータの秘匿化の向上

この実装の ConfigurationBuilder() の呼び出しでは、以下の構成プロバイダーしか呼び出しません。

.AddJsonFile(...)
.AddEnvironmentVariables();

これはつまり、開発時と単体テスト時は local.setting.json と環境変数から、Azure 上など運用環境では環境変数から、指定された値を読み出して Binding する”だけ”であることを意味しています。

もし Azure Key Vault を呼び出したいのであれば、コードをダウンロードし、Azure Key Vault 構成プロバイダーを呼び出せるように修正してください。参考: https://docs.microsoft.com/ja-jp/aspnet/core/security/key-vault-configuration?tabs=aspnetcore2x

  • 単体テストが簡単に書けるようになる

当然ですがなりません。設定値を投げ込みやすくはなりますし、xUnit を使って業務ロジック・プロジェクトの単体テストを実施しやすくはなりますが、その程度です。

この実装を求めるモチベーション

Azure Functions のように Binding 側基盤側でストレージ/リポジトリへのI/O を隠蔽してくれる実行基盤は、単調で繰り返しが多く似たようなコードを大量生産しがちなI/O処理を書かずとも実装が出来るので、大変ありがたい話です。ですが、結果として、この隠蔽は、設定値や取得処理の調整を環境依存にさせてしまうことになり、開発工程全体を見通した場合に、その設計の自由度を奪います。

この自由度の制限は、Azure Functions のみの環境で動作すればよく、トリガ関数の範囲でトランザクションが完結するような業務処理であれば、何も問題はありません。むしろ、無駄な複雑性をユーザーサイドの業務コードに持ち込まないことで、全体がシンプルになり見通しが良くなるはずです。

しかしながら、複雑な業務整合性を担保するために、独自の struct や class を作成し、処理全体を通してより良く調整された業務ロジックが既に存在している場合や、これから書くコードの一部を再利用する可能性を検討している場合には、この隠蔽が邪魔にあることがあります。

Disposable なコンポーネントを指向する場合、前者のアプローチを前提とし、業務整合性の範囲を極限まで狭めた上で、冪等性原則のストア・モデル を用いて、疎結合に、そしてスケールしやすく実装することが望ましいはずです。

一方で、金融業務など、整合性の担保と業務仕様とが密に結合し、整合性を担保する範囲が拡がり気味でかつ複雑で、実行から完了までの実行状態の推移を高い精度で予測したい場合には、後者のアプローチが増えてきます。

結果として、後者が増えがちな開発現場では、業務ロジックは Functions のトリガ関数のエントリ・ポイント メソッド内に記載せず別アセンブリとして作成した上で参照するような実装になることが多いでしょう。

そして、この「業務ロジックの分離」への期待は、ASP.NET Core でも同様に起こります。

ASP.NET Core においては、Controller のコンストラクタでDIにより受け取る強い型付けされたオプションは、ConfigurationBuilder() により生成されたオブジェクトを受け取ることが、既定の実装設計の方針になっています。

ASP.NET Core から入った場合、業務ロジック用の型付けされたオプションクラスを業務アセンブリに渡すような設計を思いつき、そして実装するでしょう。もし、業務ロジックがDIパターンで作成されていたとしても同様です。挿入される業務ロジックはテストされるべきであり、その場合、依存するオブジェクトをモックで渡すにしても、その設定値(または数多の設定値を取得するためのシードとなる設定値)をどこかで渡す必要があり、結果として、型付けされたオプションクラスを渡すような設計に着地するでしょう。

Azure Functions をコーディングレス、ミニマム・コーディング、ウェブブラウザ・オンリー開発が出来るようにしたい、つまり、Binding で I/O を隠蔽してしまいたい以上は、そのパラメータ設定をポータルから指定・変更することが出来る必要があります。

このパラメータ設定を指定しているのは、ビルド時に生成される、トリガ関数名のフォルダに配置された function.json です。Azure Portal から作成した場合でも kudu でフォルダを徘徊すれば発見することが出来るでしょう。Visual Studio の場合なら、Azure Functions SDK に依存している Functions 用プロジェクトの bin 配下(の下の下の…にある関数名フォルダ)に、publish 時に出力されているようです。Azure Portal では、ブラウザ画面からこのファイルを書き換えることで、バインドの動作を変更させることが出来ます。

ただ、これらの設定は、Binding 個々の設定・振る舞いを変えるために存在しており、実行環境全体で指定したい設定については、host.json や環境変数を用いて調整されることになります。

バインディングの値は、初期値は C# のコードから指定することが出来るのですが、host.json やアプリケーション設定の値については、コード外から指定することが大半だと思います。

Azure Functions では、このように設定値の管理と指定が、それぞれの役割や目的に応じて実装方法や指定方法に違いがあるため、やや煩雑です。

そしてこの違いは、Azure Functions を前提に開発しつつ、業務ロジック部分を単体テストしようと分離しつつ、xUnit などを用いてテストしようとすると、さらに複雑さを増していきます。

トリガ関数のエントリポイントには、業務プロセスの外側にあるトリガ条件(タイミングや外部システム依存のイベント発火など)が定義されているため、ここを起因に(半ば結合的に)テストをすることが困難です。

このあたりの困難さは、Visual Studio で Functions のホストとなるプロジェクトと業務ロジックのプロジェクトを分離しながら書いていくと、早々に気がつくはずです。

「あっれー…こっちじゃないのか、この実行基盤の気持ちは…」

で。

今回の実装は、その違いを ASP.NET Core 側に似せて、動作そのものは Functions のバインディングとして実装し隠蔽した上で、実装者は、ASP.NET Core の Controller クラスでの実装のように強い型付けされたオプションを業務ロジック側に受け渡すだけで良い、となるような呼び出し方を意識して、作成されています。

既に ASP.NET Core でそれなりの業務ロジックを書いている場合、ストレージ・各種ストアへの I/O は、自身で書いていることが大半でしょう。その場合、Azure Functions を使わずとも Web API として実装することも可能だと思います。 App Service Web Apps を利用すれば、実際の運用上は事実上のサーバーレス実行環境です。Functions がその上に実装されているわけですし。

ですが、外部サブシステムとの連携の都合や、トリガ機能など特殊なバインディングに依存した上で、別途作成した再利用前提の汎用的な業務ロジックを呼び出すような設計に挑戦したい場合もあるでしょう。あえて Azure Functions を使うこともあるはずで、その場合に、 Azure Functions の癖のある設定値の管理を少し捨てて、自分の管理下で制御してしまいたい、という気持ちに、すこしだけ応えられるのでは、というモジュールが、今回の CbOptionsCore です。

基本的な概念

以下の ASP.NET Core の構成に似たイメージで利用することを意識しています。

ASP.NET Core の設定呼び出しのスタックの例

  • 設定値(または設定値のシードとなる設定)はそれぞれ、
    ・ 開発時は appsettings.[some.]json を ConfigrationBuilder() で参照
    ・単体テスト時は専用のjsonまたは上記のそれを ConfigrationBuilder() で参照
    ・運用時は Azure Portal の Application Settings で指定しConfigrationBuilder() で参照
    する。
  • Controller クラスでは、DIで渡されたオプションクラスを業務ロジック側に受け渡す。または、業務ロジック側でもDIで受け取る。

Azure Functions 2.0 + CbOptionsCore での設定呼び出しのスタックの例

  • 設定値(〃)はそれぞれ、
    ・ 開発時は local.settings.json を CbOptionsCore 経由で参照
    ・単体テスト時は専用のjsonまたは上記のそれを CbOptionsCore 経由で参照
    ・運用時は Azure Portal の Application Settings で指定し CbOptionsCore 経由で参照
    する。
    CbOptionsCore はConfigrationBuilder() のラッパーとして便宜を図るだけ。
  • トリガ関数のエントリポイントメソッドでは、パタメータで渡されたオプションクラスを業務ロジック側に受け渡す。

基本的な使い方

拙い英語なので読みにくいですが、ここを読んで下さい。

ようは、一般的なトリガ関数の定義は以下のようなものですが、

以下のように引数にパラメータを追加することで、メソッドに入った時点で、options に型付けされたオプションを取得することが出来ます。

サンプルの全体はこちら。

実態は ConfigurationBuilder() に対する .AddJsonFile() と .AddEnvironmentVariables() のラッパーですので、.AddJsonFile() 向けに、同様のパラメータセットを指定することが可能です。

実装上のもろもろ

CbOptions の指定パラメータごとに IConfiguration のインスタンスを保持します

ASP.NET Core では、実質的にSingleton状態になる Startup クラスで保持されている IConfiguration のインスタンスですが、通常はJSONファイルの読み込み設定は単一です。

Azure Functions のパラメータとして処理する以上は複数のパラメータ状態での読み込みが起こり得るため、設定値の状態をキーとして複数保持するようにしています。これにより、関数の呼び出しごとに毎回 .Build() を呼び出すことはありません。この使い方が正しいのかは、確信を持ってないので、先々コード見て駄目そうなら直します。

xUnit もろもろ

単体テストで xUnit のプロジェクトを追加してもろもろする際、期待通りに動かないことがあると思います。サンプルプロジェクトのように、 ターゲットが netcoreapp2.0 であることを確認し、また、出力を受け取るために以下を参考にして調整してあげてください。

あと、CbOptionsCore は、実行時のカレントディレクトリ配下にある、local.settings.json か別途指定したファイル名の json を探します。ですので、単体テストの際、なんとかして単体テストのプロジェクトの直下にファイルを配置してあげる必要があります。

サンプルでは、単体テスト用の test.jsettings.json を配置して、ビルド時にコピーするようにしています。ここ、開発中の Functions プロジェクトの local.settings.json をなんとかして見るなり、テスト用のバッチコマンドで拾ってくるなど、いろいろやりようはあるので、おまかせです。

もし、Run() などのメソッドを叩いてテストしたい、という場合に困るのが、エントリポイントメソッドのパラメータにある TraceWriter() です。

これを単体テストの空間で生成して渡してあげないといけないので、うぇぇぇこれどうすんべ、となるのですが、 以下のアプローチで行けます。

TraceWriter を継承した自前のクラスを作成し、そのクラスの初期化を、xUnit の出力キャプチャを渡してあげるタイミングで同様に渡してあげます。TraceWriter として渡した先で書いた内容は、xUnit が渡してくれた ITestOutputHelper に入るので、Visual Studio のテスト結果として Output / 出力 に表示されます。(GitHub 上の Sample.SampleTests では、定義してあるものの、呼び出してはいません。改造して試してみてください)

Appndeix.

参考に。公式ドキュメントです。

Azure Functions 1.0 向けで似たことを思いついた人のブログ記事。

CboptionsCore のポジトリは以下。

Have a Happy .NET Core Functions Life !!

Written by

President of team Sirocco, LLC / Financal Technology を中心に、技術界隈を実務もやりつつ見続けてます。

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store