ASP.NET Web API でのモデルの検証

この記事では、モデルに注釈を付け、データ検証に注釈を使用し、Web API で検証エラーを処理する方法について説明します。 クライアントから Web API にデータが送信されたときには、多くの場合、処理を実行する前にデータを検証する必要があります。

データの注釈

ASP.NET Web API では、System.ComponentModel.DataAnnotations 名前空間の属性を使用して、モデルのプロパティの検証規則を設定できます。 次の モデルがあるとします。

using System.ComponentModel.DataAnnotations;

namespace MyApi.Models
{
    public class Product
    {
        public int Id { get; set; }
        [Required]
        public string Name { get; set; }
        public decimal Price { get; set; }
        [Range(0, 999)]
        public double Weight { get; set; }
    }
}

ASP.NET MVC でモデル検証を使用したことがある場合、これには見覚えがあるはずです。 Required 属性は、Name プロパティを null にしてはならないことを示します。 Range 属性は、Weight が 0 から 999 までの範囲である必要があることを示します。

クライアントが、次の JSON 表現を使用して POST 要求を送信するとします。

{ "Id":4, "Price":2.99, "Weight":5 }

クライアントに必須としてマークされている Name プロパティが含まれていないことがわかります。 Web API は、JSON を Product インスタンスに変換するときに、検証属性に対して Product の検証を行います。 コントローラー アクションでは、モデルが有効かどうかをチェックできます。

using MyApi.Models;
using System.Net;
using System.Net.Http;
using System.Web.Http;

namespace MyApi.Controllers
{
    public class ProductsController : ApiController
    {
        public HttpResponseMessage Post(Product product)
        {
            if (ModelState.IsValid)
            {
                // Do something with the product (not shown).

                return new HttpResponseMessage(HttpStatusCode.OK);
            }
            else
            {
                return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
            }
        }
    }
}

モデルの検証では、クライアント データが安全であるとは限りません。 アプリケーションの他のレイヤーでは、追加の検証が必要になる場合があります。 (たとえば、データ レイヤーで外部キーの制約が適用される場合があります)このチュートリアル「Entity Framework での Web API の使用」では、これらの問題の一部について説明します。

"Under-Post": クライアントが一部のプロパティを除外すると、過少転記が行われます。 たとえば、クライアントが次を送信するとします。

{"Id":4, "Name":"Gizmo"}

ここでは、クライアントは Price または Weight の値を指定しませんでした。 JSON フォーマッタは、不足しているプロパティに既定値 0 を割り当てます。

Screenshot of code snippet with Product Store dot Models dot Product's drop-down menu options over it.

モデルの状態は有効です。これは、0 がこれらのプロパティの有効な値であるためです。 これが問題であるかどうかは、シナリオによって異なります。 たとえば、更新操作では、"0" と "未設定" を区別できます。クライアントに値の設定を強制するには、プロパティを null に設定し、Required 属性を設定します。

[Required]
public decimal? Price { get; set; }

"Over-Posting": クライアントは、予想よりも多くのデータを送信することもできます。 次に例を示します。

{"Id":4, "Name":"Gizmo", "Color":"Blue"}

ここでは、JSON には、Product モデルに存在しないプロパティ ("Color") が 含まれています。 この場合、JSON フォーマッタはこの値をただ無視します。 (XML フォーマッタも同じです)過剰転記を行うと、モデルに読み取り専用のプロパティがある場合に問題が発生します。 次に例を示します。

public class UserProfile
{
    public string Name { get; set; }
    public Uri Blog { get; set; }
    public bool IsAdmin { get; set; }  // uh-oh!
}

ユーザーが IsAdmin プロパティを更新して管理者に昇格するのを望まない場合。 最も安全な方法は、クライアントの送信が許可されているものと正確に一致するモデル クラスを使用することです。

public class UserProfileDTO
{
    public string Name { get; set; }
    public Uri Blog { get; set; }
    // Leave out "IsAdmin"
}

Note

Brad Wilson のブログ投稿 "入力検証とASP.NET MVC でのモデル検証" では、過少投稿と過剰投稿についてよく説明しています。 この投稿は MVC 2 ASP.NET に関するものの、問題は引き続き Web API に関連しています。

検証エラーの処理

検証に失敗しても、Web API はクライアントにエラーを自動的に返しません。 モデルの状態をチェックし、適切に応答するのは、コントローラー アクション次第です。

コントローラー アクションが呼び出される前にモデルの状態をチェックするアクション フィルターを作成することもできます。 次に例を示します。

using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using System.Web.Http.ModelBinding;

namespace MyApi.Filters
{
    public class ValidateModelAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            if (actionContext.ModelState.IsValid == false)
            {
                actionContext.Response = actionContext.Request.CreateErrorResponse(
                    HttpStatusCode.BadRequest, actionContext.ModelState);
            }
        }
    }
}

モデルの検証に失敗した場合、このフィルターは検証エラーを含む HTTP 応答を返します。 その場合、コントローラー アクションは呼び出されません。

HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Date: Tue, 16 Jul 2013 21:02:29 GMT
Content-Length: 331

{
  "Message": "The request is invalid.",
  "ModelState": {
    "product": [
      "Required property 'Name' not found in JSON. Path '', line 1, position 17."
    ],
    "product.Name": [
      "The Name field is required."
    ],
    "product.Weight": [
      "The field Weight must be between 0 and 999."
    ]
  }
}

このフィルターをすべての Web API コントローラーに適用するには、構成時にフィルターのインスタンスを HttpConfiguration.Filters コレクションに追加します。

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Filters.Add(new ValidateModelAttribute());

        // ...
    }
}

もう 1 つのオプションは、個々のコントローラーまたはコントローラー アクションの属性としてフィルターを設定することです。

public class ProductsController : ApiController
{
    [ValidateModel]
    public HttpResponseMessage Post(Product product)
    {
        // ...
    }
}