しばやん雑記

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

Azure Functions でも HttpTrigger でモデルバインディングを使って楽をしたい

ASP.NET Core MVC だと普通に使っているモデルバインディングですが、Azure Functions の HttpTrigger でも近い形で使えるようになってます。テンプレートでは HttpRequest から自前でパースしてますが、ランタイムに任せることが出来ます。

使い方は Core MVC と同様に、単純に受け取りたいクラスを Function の引数として追加するだけです。

public static class Function1
{
    [FunctionName("Function1")]
    public static IActionResult Run(
        [HttpTrigger(AuthorizationLevel.Function, "post")] DemoModel model,
        ILogger log)
    {
        return new OkObjectResult($"Hello, {model.Name}");
    }
}

public class DemoModel
{
    public string Name { get; set; }
    public int Age { get; set; }
}

この HttpTrigger に対して JSON を POST で投げると、値がバインドされたインスタンスを受け取れます。

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

これで毎回 HttpRequest から Body を読み取ってデシリアライズするという手間が省けて、実装がスッキリとします。POST の Body だけではなくクエリパラメータの値もバインドしてくれるのも便利。

上の例のように単純な型であれば問題ないのですが、残念ながら対応していない型があります。今のところ確認しているのは配列と列挙型です。

モデルを少し書き換えて配列のプロパティを追加してみます。

public class DemoModel
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string[] Friends { get; set; }
}

Core MVC なら問題なくバインドしてくれるのですが、Azure Functions では null になります。

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

そして更に列挙型のプロパティを追加して同じように試してみます。

public class DemoModel
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string[] Friends { get; set; }
    public JobType Job { get; set; }
}

public enum JobType
{
    社畜
}

こっちは配列よりも酷く、以下のようなキャスト失敗の例外を吐いて 500 を返します。

System.Private.CoreLib: Exception while executing function: Function1. Microsoft.Azure.WebJobs.Host: Exception binding parameter 'model'. System.Private.CoreLib: Invalid cast from 'System.String' to 'FunctionApp31.JobType'.

当然ながら Core MVC では問題なくバインドできるようなケースです。HttpTrigger の場合は Core MVC では扱えていた型でも、バインドできなかったりそもそもエラーになってしまうという面倒な問題があります。

回避策と原因

そして回避策ですが、HttpTrigger の実装を調べたところ Core MVC から追加された InputFormatter を使うコードになっていたのに、肝心の Formatter が DI に登録されていなかったので動いてませんでした。*1

以下のように IWebJobsStartup を実装して JsonInputFormatter を追加すると動くようになります。

[assembly: WebJobsStartup(typeof(FunctionApp31.JsonWebJobsStartup))]

namespace FunctionApp31
{
    public class JsonWebJobsStartup : IWebJobsStartup
    {
        public void Configure(IWebJobsBuilder builder)
        {
            builder.Services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, MvcJsonMvcOptionsSetup>());
        }
    }
}

更に残念なことに .NET Core 2.1 向けになった Azure Functions v2 では、MetadataGenerator の問題により IWebJobsStartup が起動時に実行されないという不具合があります。問題は認識しているようですが、今のところは TFM を netstandard2.0 に変更するしか回避策がありません。

これでやっと配列や列挙型も Core MVC の InputFormatter を使ってバインドされるようになります。

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

このように残念な結果になった原因ですが、そもそも Azure Functions の HttpTrigger がバインディングに Core MVC の機能を使わずに自前で実装していることにあります。

何で独自に実装しているのか分からないですが、とにかくその実装が中途半端で配列と列挙型を考慮していないため、意図したとおりの挙動にならないのでした。

おまけ : モデルバリデーション

モデルバインディングを行えば、次はバリデーションを追加したくなるはずです。牛尾さんが Data Annotations を使った方法を既に書いてくれているので参照してください。

HttpTrigger が公式にバリデーションを提供してくれると嬉しいのですが、難しいでしょうね。

*1:該当リポジトリに Issue と Pull Request は投げてあります