ASP.NET Web API と DataAnnotations でモデルバリデーションする

ASP.NET MVC では DataAnnotations を使ってバリデーションを行うのが一般的ですが、 ASP.NET Web API ではどうやってやるのか調べてみました。 Visual Studio 2013 で確認しています。

要件としては以下のような感じとします。

  • Controller の引数でクラスを受け取り、クラスに対して属性を付与してバリデーションする
  • バリデーションエラーがある場合、エラー内容とともに Bad Request(400) を返す

API のパラメーターを受けるモデルはこんな感じ。

public class SampleModel
{
    [Required]
    [Display(Name = "なにかの値")]
    public string Value { get; set; }
}

コントローラーはこんな感じ。属性ルーティングを使っています。

[RoutePrefix("api")]
public class SampleController : ApiController
{
    [Route("test")]
    public IHttpActionResult PostTest(SampleModel model)
    {
        if (ModelState.IsValid == false)
        {
            return new ResponseMessageResult(Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState));
        }
        // 略
    }
}

MVC のときと基本的に同じですね。 /api/test に Accept: application/json ヘッダーを付けて Value=という感じの POST を投げると以下の様な結果が返ってきます。レスポンスを返すところは Request.CreateResponseと間違えやすいので気をつけましょう(間違えてはまりかけた)。

{"Message":"The request is invalid.","ModelState":{"model.Value":["なにかの値 フィールドが必要です。"]}}

検証失敗時の動作はどのコントローラーでも同じとしたいので、処理を共通化したいところです。 ASP.NET MVC と同様、 ActionFilter が使えるので、これを使って共通化するのが良さそうですね。 MVC のものとは名前空間が違うので注意。

上記ページの例ほぼそのままですが、以下の様な感じで ActionFilter を作ります。

public class ApiValidationAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext)
    {
        var modelState = actionContext.ModelState;
        if (modelState.IsValid == false)
        {
            actionContext.Response = actionContext.Request
                        .CreateErrorResponse(HttpStatusCode.BadRequest, modelState);
        }
        base.OnActionExecuting(actionContext);
    }
}

Controller のメソッドに属性を付けます。デフォルトで有効にしたいのであれば、 Global.asax あたりで GlobalConfiguration に設定するのが良いでしょう。

    [Route("test")]
    [ApiValidation]
    public IHttpActionResult PostTest(SampleModel model)

パラメーターが空の場合の対応

モデルにバインドするパラメーターを何も指定しない場合、コントローラーのメソッド引数であるモデルクラスが null になります。この場合、 ModelState.IsValidtrue を返してくるので、入力値検証が働きません。この場合でも検証エラーとしたいところです。

ActionFilter の処理に以下の様な感じでチェックを追加することで対応できるようです。

if (actionContext.ActionArguments.Any(kv => kv.Value == null))
{
    actionContext.Response = actionContext.Request
        .CreateErrorResponse(HttpStatusCode.BadRequest, "パラメーターが指定されていません。");
}

これで以下の様な結果が返ります。

{"Message":"パラメーターが指定されていません。"}

以上で、 ASP.NET Web API でも DataAnnotations を使ってモデルバリデーションすることができました。結果データが .NET 風味あふれる感じになっているあたりが課題ですかね。少なくともキー名は自分でカスタマイズしたいところです。そのへんについては別途調査したいと思います。