しばやん雑記

ASP.NET Core とメイドさんが大好きなフリーランスのプログラマーのブログ

Azure Functions v2 でインスタンスメソッドも Function として利用可能に

先週リリースされた Azure Functions Runtime 2.0.12265 からは、インスタンスメソッドでも Function として扱われるようになりました。これまでは静的メソッドしか使えなかったので、モックしにくいとかいろいろ言われていたと記憶しています。

リリースノートにひっそりと載っていますが、おそらく DI 周りサポートに向けて作業の途中なのでしょう。

まだ全てのリージョンにデプロイはされていないみたいですが、来週までには完了しそうです。

昨日ぐらいにようやく Visual Studio 向けにも 2.0.12265 が入った Functon Tools がリリースされたので、実際にローカルで動作を確認しておきました。結論としては DI 周りがサポートされると、これまでの不満点の改善が期待できる感じがしてきました。

限定的に DI が使えるように

これまでの Function は静的メソッド限定だったので、ASP.NET Core では一般的に書いているような DI を使ったコンストラクタへのインスタンスの注入は行えなかったですが、限定的ですが利用出来るようになりました。ただし公式にサポートされた使い方ではないです。

具体的には Azure Functions Runtime 自体が DI に追加したクラスの場合、コンストラクタインジェクションで受け取れるようになっています。

public class Function1
{
    public Function1(HttpClient httpClient)
    {
        // Function Runtime 側が DI に追加しているのでシングルトンな HttpClient インスタンスを受け取れる
        _httpClient = httpClient;
    }

    private readonly HttpClient _httpClient;
}

詳しい方なら WebJobsStartup を使えば DI への追加が行えると思われるでしょうが、Azure Functions Runtime は DI を 2 系統持っているのが問題となります。

ここでは便宜的に WebHost 側と WebJobs 側と呼びますが、WebJobsStartup で渡される IServiceCollection は WebJobs 側の DI になり、実際に Function のクラスが解決される際は WebHost 側の DI が使われるので、現時点では手の出しようがない部分となります。公式にサポートされるのを待つしかないです。

ユーザー側で DI への追加は出来ないですが、試していると IConfiguration は Functions Runtime 側で追加されているので受け取れることが分かりました。テスト用に以下のようなコードを書きました。

public class Function1
{
    public Function1(IConfiguration configuration)
    {
        var options = configuration.GetSection("Kazuakix")
                                   .Get<SampleOptions>();
    }
}

特定のセクションの値を用意しておいたクラスにバインドするという、簡単かつ良くあるコードです。ちなみに ASP.NET Core のお作法的には IOption<T> で受け取りたいところですが、今は無理です。

そして local.settings.json には 2 つほどキーと値を追加しておきました。

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "Kazuakix:Age": 50,
    "Kazuakix:Job": "Syachiku"
  }
}

この Functions をデバッグ実行してみると、コンストラクタインジェクションが行われて IConfiguration を取得でき、さらに local.settings.json に追加した値がバインドされたインスタンスを取得できました。

f:id:shiba-yan:20190120145742p:plain

Azure Functions v2 では local.settings.json や本番から設定を読み込む手段が公式に提供されていないという謎があったので、以下のエントリのように手動で ConfigurationBuilder を使って読み込んだりしていましたが、DI がサポートされると必要なくなりそうです。

DI が 2 系統に分かれているのは割と混乱とバグを生んでいる感じがするので、将来的には改善されると良いですね。複雑すぎて、正直なところ PR を送る気にはならないのが辛いです。

テスタビリティの改善

静的メソッドで書いていると外部から簡単にモックしたサービスなどのインスタンスを注入できないので、テストガチ勢からは不評だったのですがインスタンスメソッドになったので、この辺りが改善されました。

ただし今はカスタムクラスを DI に追加できないのと、ASP.NET Core の DI は前にも書いたように制約があるので、利用するためには少し工夫する必要があります。

Poor Man's DI あるいは Pure DI のようにコンストラクタのオーバーロードを用意できないので、上のエントリで書いたようにデフォルト値を使ってインスタンスを作成するようにすれば、外部からサービスを注入出来るのでモック周りが楽になります。

public class Function1
{
    public Function1(ISampleService sampleService = null)
    {
        _sampleService = sampleService ?? new DefaultSampleService();
    }

    private readonly ISampleService _sampleService;

    [FunctionName("Function1")]
    public IActionResult Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)]
        HttpRequest req,
        ILogger log)
    {
        var name = req.Query["name"];

        if (string.IsNullOrEmpty(name))
        {
            return new BadRequestResult();
        }

        return new OkObjectResult(_sampleService.Hello(name));
    }
}

Azure Functions Runtime 上で実行される場合には null になるので、デフォルトの実装が使われます。モックを使う場合にはコンストラクタでインスタンスを渡してあげれば良いので、リフレクションとかこねくり回すより圧倒的にシンプルになります。

Build 2018 か Ignite 2018 のセッションで Azure Functions v2 での DI サポートのデモをやっていたので、将来的には DI が公式に提供されるはずですが、Configuration 周りは今の状態でも使いたくなる感じです。