しばやん雑記

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

Azure Functions v3 向けに REST API を作りやすくするサンプルコードを書いた

最近は Azure Functions でサクッと HttpTrigger を使って REST API を書くことが多いですが、ASP.NET Core のように API 実装に必要な機能が揃っていないので、毎回同じようなコードを書いて対応してました。

具体的にはリクエストをモデルにバインドする部分や、そのバインドとされたモデルに対するバリデーションといった機能です。ASP.NET Core なら何も考えなくても良い部分ですが、Azure Functions では対応していないので再利用可能なサンプルコードを用意しました。

使い方は簡単で、これまで HttpTrigger の実装を行っていた Function のクラスを HttpFunctionBase を継承するようにします。DI を使っているので、static を削除する必要があるはずです。

コンストラクタも追加が必要ですが、Visual Studio や ReSharper を使っていれば自動実装されるはずです。

public class Function1 : HttpFunctionBase
{
    public Function1(IHttpContextAccessor httpContextAccessor)
        : base(httpContextAccessor)
    {
    }

    [FunctionName("Function1")]
    public IActionResult Run(
        [HttpTrigger(AuthorizationLevel.Function, "post")]
        HttpRequest req,
        ILogger log)
    {
        return Ok("Hello, world");
    }
}

この HttpFunctionBase クラスは以下のような機能を実装しています。大体は ASP.NET Core の ControllerBase に近くなるように実装していますが、メソッド数は全然少ないです。

  • IActionResult を作成するためのヘルパーメソッド
  • Request / Response / User へ簡単にアクセスするためのプロパティ
  • モデルのバリデーションを実行し ModelState に結果を格納するメソッド

利用可能なメソッド一覧は実装を見てもらえれば良いです。REST API の実装に便利そうなメソッドのみをピックアップして実装しています。

サンプルコードに用意している例を挙げると、POST で送信されたデータを C# のクラスにバインドし、検証属性を使ってバリデーションを行うという非常に一般的な処理が以下のようにシンプルに書けています。

あまり知られていない気がしますが HttpTrigger を独自のクラスに付けると、いい感じにリクエストから値をバインドしてくれます。なので ASP.NET Core っぽく書けています。

public class Function1 : HttpFunctionBase
{
    public Function1(IHttpContextAccessor httpContextAccessor)
        : base(httpContextAccessor)
    {
    }

    [FunctionName("Function1")]
    public IActionResult Run(
        [HttpTrigger(AuthorizationLevel.Function, "post")]
        SampleModel model,
        ILogger log)
    {
        if (!TryValidateModel(model))
        {
            return BadRequest(ModelState);
        }

        return Ok(model);
    }
}

public class SampleModel
{
    [Required]
    public string Name { get; set; }

    [Range(100, 10000)]
    public int Price { get; set; }
}

ただしバリデーションは自動で行うことはできないため、明示的に TryValidateModel メソッドを呼び出しています。バリデーション結果は ASP.NET Core と同様に ModelState へ格納されるので、ヘルパーメソッドを使えばそのまま検証結果を返せます。

実際に空っぽのリクエストを投げてみると、以下のように 400 エラーと検証結果が返ってきます。

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

もちろん正しい形式のリクエストを投げると 200 とコンテンツが返ってくるようになります。

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

Azure Functions v3 から配列や enum のバインディングが正しく行えない問題が修正されたため、今回のような実装が利用できるようになりました。

地味に使い勝手が悪かったのが生の RequestResponse を触る場合でしたが、ASP.NET Core と同様のプロパティを用意して扱いやすくしました。ログイン中のユーザー情報も User でサクッと扱えます。

以下はレスポンスにヘッダーを付ける例ですが、特に違和感ないコードで完結しています。

public class Function2 : HttpFunctionBase
{
    public Function2(IHttpContextAccessor httpContextAccessor)
        : base(httpContextAccessor)
    {
    }

    [FunctionName("Function2")]
    public IActionResult Run(
        [HttpTrigger(AuthorizationLevel.Function, "get")]
        HttpRequest req,
        ILogger log)
    {
        Response.Headers.Add("Cache-Control", "no-cache");

        return Ok($"Now: {DateTime.Now}");
    }
}

実行してみると、ちゃんとヘッダーが付いていることが確認できます。単純なコードでした。

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

しっかり試したわけではないですが PipeWriter を使って応答に直接コンテンツを書き込むことも可能そうでした。その場合は voidTask を返す Function を定義すればよい感じでした。

余力があれば Azure Functions の DI / Configuration 周りを含めた NuGet パッケージとして公開します。