WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

ASP.NET Web API のバインディング

by WebSurfer 2018年9月11日 16:52

ASP.NET Web API でもクライアントから送信されてきたデータはアクションメソッドの引数にバインドされますが、その仕組みは ASP.NET MVC とは異なるということを書きます。

ASP.NET Web API

詳しくは MSDN Blog の記事「How WebAPI does Parameter Binding」に書いてありますのでそちらを読んでもらうとして、要点だけ書きますと以下の通りです。

  1. Model Binding または Formatters を利用するという 2 つの方法があって、Web API の場合は、クエリ文字列からパラメータを取得する場合は Model Binding を、ボディから取得する場合は Formatter を使う。
  2. string などのプリミティブ型をアクションメソッドの引数にした場合、引数に属性が付与されてなければクエリ文字列からパラメータを探してモデルバインディングする。
  3. コンプレックス型(例:記事の Customer クラス、下のコードの Hero クラス)の場合、デフォルトでは Formatter を使ってボディからパラメータを取得する。

上記の点を踏まえたサンプルコードを以下にアップしておきます。

Model

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Runtime.Serialization;

namespace WebAPI.Models
{
    public class Hero
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
}

Api Controller

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using System.Web;
using WebAPI.Models;
 
namespace WebAPI.Controllers
{
    public class ValuesController : ApiController
    {
        private List<Hero> heroes = new List<Hero> {
              new Hero {Id = 1, Name = "スーパーマン"},
              new Hero {Id = 2, Name = "バットマン"},
              new Hero {Id = 3, Name = "ウェブマトリクスマン"},
              new Hero {Id = 4, Name = "チャッカマン"},
              new Hero {Id = 5, Name = "スライムマン"}
          };

        // GET api/values (Read...すべてのレコードを取得)
        public List<Hero> Get()
        {
            return heroes;
        }

        // GET api/values/5 (Read...id 指定のレコード取得)
        public Hero Get(int id)
        {
            return heroes[id - 1];
        }

        // POST api/values  (Create...レコード追加)
        public List<Hero> Post(Hero postedHero)
        {
            heroes.Add(postedHero);
            return heroes;
        }

        // PUT api/values/5 (Update...id 指定のレコード更新)
        public List<Hero> Put(int id, [FromBody] string name)
        {
            heroes[id - 1].Name = name;
            return heroes;
        }

        // DELETE api/values/5 (Delete...id 指定のレコード削除)
        public List<Hero> Delete(int id)
        {
            heroes.RemoveAt(id - 1);
            return heroes;
        }
    }
}

上の Api を呼び出すスクリプト

<input type="button" value="READ ALL"
       onclick="apiHeroesGet();" />
<input type="button" value="READ 5"
       onclick="apiHeroesGet5();" />
<input type="button" value="UPDATE 5"
       onclick="apiHeroesPut5();" />
<input type="button" value="DELETE 5"
       onclick="apiHeroesDelete5();" />
<input type="button" value="CREATE 6"
       onclick="apiHeroesPost();" />

<ul id="heroes"></ul>

@section Scripts {
  <script type="text/javascript">
  //<![CDATA[

    function apiHeroesGet() {
      $.ajax({
        type: "GET",
        url: "api/values",
        success: function (data, textStatus, jqXHR) {
          $('#heroes').empty();
          $.each(data, function (key, val) {
            var str = val.Id + ': ' + val.Name;
            $('<li/>', { html: str }).appendTo($('#heroes'));
          });
        },
        error: function (jqXHR, textStatus, errorThrown) {
          $('#heroes').empty();
          $('#heroes').text('textStatus: ' + textStatus +
              ', errorThrown: ' + errorThrown);
        }
      });
    }

    function apiHeroesGet5() {
      $.ajax({
        type: "GET",
        url: "api/values/5",
        success: function (data, textStatus, jqXHR) {
          $('#heroes').empty();
          var str = data.Id + ': ' + data.Name;
          $('<li/>', { html: str }).appendTo($('#heroes'));
        },
        error: function (jqXHR, textStatus, errorThrown) {
          $('#heroes').empty();
          $('#heroes').text('textStatus: ' + textStatus +
              ', errorThrown: ' + errorThrown);
        }
      });
    }

    function apiHeroesPut5() {
      $.ajax({
        type: "PUT",
        url: "api/values/5",
        data: encodeURI("=ガッチャマン"),

        // 以下のコードがあるとバインドできず null が渡される
        //contentType: "application/json; charset=utf-8",

        success: function (data) {
          $('#heroes').empty();
          $.each(data, function (key, val) {
            var str = val.Id + ': ' + val.Name;
            $('<li/>', { html: str }).appendTo($('#heroes'));
          });
        },
        error: function (jqXHR, textStatus, errorThrown) {
          $('#heroes').empty();
          $('#heroes').text('textStatus: ' + textStatus +
             ', errorThrown: ' + errorThrown);
        }
      });
    }

    function apiHeroesDelete5() {
      $.ajax({
        type: "DELETE",
        url: "api/values/5",
        success: function (data) {
          $('#heroes').empty();
          $.each(data, function (key, val) {
            var str = val.Id + ': ' + val.Name;
            $('<li/>', { html: str }).appendTo($('#heroes'));
          });
        },
        error: function (jqXHR, textStatus, errorThrown) {
          $('#heroes').empty();
          $('#heroes').text('textStatus: ' + textStatus +
              ', errorThrown: ' + errorThrown);
        }
      });
    }

    function apiHeroesPost() {
      var j = { Id: 6, Name: "ガッチャマンの息子" };
      var jsonString = JSON.stringify(j);
      $.ajax({
        type: "POST",
        url: "api/values",
        data: jsonString,
        contentType: "application/json; charset=utf-8",
        success: function (data) {
          $('#heroes').empty();
          $.each(data, function (key, val) {
            var str = val.Id + ': ' + val.Name;
            $('<li/>', { html: str }).appendTo($('#heroes'));
          });
        },
        error: function (jqXHR, textStatus, errorThrown) {
          $('#heroes').empty();
          $('#heroes').text('textStatus: ' + textStatus +
              ', errorThrown: ' + errorThrown);
        }
      });
    }

  //]]>
  </script>
}

特に注意すべき点は Put(int id, [FromBody] string name) メソッドとそれを呼び出すスクリプトです。

アクションメソッドの引数が string 型でボディからパラメータを得たい場合は [FromBody] 属性が必要です。

さらに、name にバインドするのに Formatter がどういう動きをしているのか調べ切れてませんが、自分が試した限りでは送信するデータをスクリプトの data に "name=value" と設定してはダメで、上のサンプルコードのように "=value" というように設定しないと引数 name にはバインドされませんでした。

もう一つ、Content-Type に application/json を設定するとバインドできず null が渡されてしまいます。理由は、Web API は要求ヘッダの Content-Type を見て適切な Formatter を選択するそうですが、"=value" は JSON ではないからだと思われます。

jQuery ajax で contentType を設定しないと application/x-www-form-urlencoded (デフォルト) となりますが、その場合は期待通り引数 name に "value" がバインドされます。

上記のようなトラブルを避けるために、アクションメソッドの引数は Post(Hero postedHero) と同様にコンプレックス型(Hero クラス)とし、スクリプトのapiHeroesPost() のように JSON 文字列を送信するようにした方が良いかもしれません。

Tags: ,

MVC

Web API で AuthorizationFilter 実装

by WebSurfer 2016年4月10日 14:39

MSDN Forum の「ASP.net MVC5 WEB APIのキャンセル(特定のデータの返却)する方法はありますか?」という表題のスレッドでの話です。

[クライアント]⇒[閲覧用ページ]⇒[Web API]⇒[DB サーバー]

という構成で、(1) [Web API]が要求を受けたらそこで時間外か否かを判断し、(2) 時間外であれば[Web API]のアクションメソッドは実行せず、(3) [閲覧用ページ]に時間外メッセージと遷移先の URL を応答として返し、(4) [閲覧用ページ]は応答の内容をチェックして時間外であれば遷移先に指定された URL に JavScript で遷移する・・・という方法を考えてみます。

もちろん時間外でなければ、上記で、(2)[Web API]でアクションメソッドを実行し、(3) 通常の応答を[閲覧用ページ]に返し、(4)[閲覧用ページ]は応答を処理して表示するということになります。

それを実現するためには、[Web API]において以下のような AuthorizationFilterAttribute を継承したフィルターを作って、OnAuthorization メソッドを override し、そこでアクションメソッドが実行される前に時間外か否かをチェックし、時間外であればその旨メッセージを返すようにします。(以下のコードは既存のコードを流用したため日付の範囲で制限してます)

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

namespace WebApi1.Filters
{
  [AttributeUsage(
       AttributeTargets.Method | AttributeTargets.Class,
       Inherited = true, AllowMultiple = false)]
  public class TimeLimitAttribute : AuthorizationFilterAttribute
  {
    public TimeLimitAttribute(string dateBegin, string dateEnd)
    {
      this.Begin = dateBegin;
      this.End = dateEnd;
    }
        
    private DateTime _begin = DateTime.MinValue;
    private DateTime _end = DateTime.MaxValue;

    public string Begin
    {
      set
      {
        DateTime dateSet = DateTime.Parse(value);                
        if (dateSet >= this._end)
        {
          throw new ArgumentException("開始日設定エラー");
        }
        this._begin = dateSet;
      }            
    }

    public string End
    {
      set
      {
        DateTime dateSet = DateTime.Parse(value);
        if (dateSet <= this._begin)
        {
          throw new ArgumentException("終了日設定エラー");
        }
          this._end = dateSet;
      }
    }

    public override void OnAuthorization(
                      HttpActionContext actionContext)
    {
      if (actionContext == null)
      {
        throw new ArgumentNullException("context が null");
      }

      DateTime current = DateTime.Now;
      if (current < this._begin || current > this._end)
      {

        HttpResponseMessage response = 
            new HttpResponseMessage();
        response.Content = 
            new StringContent("時間外,<リダイレクト先 URL>");
        actionContext.Response = response;
      }

      base.OnAuthorization(actionContext);
    }        
  }
}

ASP.NET MVC 用と ASP.NET Web API 用とでは Filter は違うそうなので注意してください。詳しくは MSDN Blog の記事「Web API における操作ごとの制御 (Validation, 認証/権限, Exception 処理 など)」を見てください。

上のフィルター属性を ApiController のクラスに付与することで制限がかけられます。たとえば、以下のようにフィルター属性を付与すれば、2013/12/1 ~ 2013/12/31 以外の日時にアクセスした場合、"時間外,<リダイレクト先 URL>" という文字列が[Web API]から応答として返ってきます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using WebApi1.Models;

namespace WebApi1.Controllers
{
    [WebApi1.Filters.TimeLimit("2013/12/1", "2013/12/31")]
    public class HeroesController : ApiController
    {
        private List<Hero> heroes = new List<Hero> {
              new Hero {Id = 1, Name = "スーパーマン"},
              new Hero {Id = 2, Name = "バットマン"},
              new Hero {Id = 3, Name = "ウェブマトリクスマン"},
              new Hero {Id = 4, Name = "チャッカマン"},
              new Hero {Id = 5, Name = "スライムマン"}
          };
                
        public IEnumerable<Hero> Get()
        {
            return heroes;
        }

        public Hero Get(int id)
        {
            return heroes[id - 1];
        }

        //・・・中略・・・
    }
} 

それを[閲覧用ページ]で jQuery.Ajax + JavaScript を使って、例えば上のアクションメソッドの public IEnumerable<Hero> Get() を呼んだ場合は以下のようの処置すれば、時間外であれば[Web API]からの応答に含まれる <リダイレクト先 URL> にリダイレクトされます。

function apiHeroesGet() {
    $.ajax({
        type: "GET",
        url: "api/heroes",
        contentType: "application/json; charset=utf-8",
        success: function (data, textStatus, jqXHR) {
            if (typeof(data) == "string" && 
                data.indexOf("時間外") == 0) {
                // 時間外の場合はリダイレクト
                var results = data.split(",");
                window.location.href = results[1];
            }
            else {
                // 時間内の場合の処置(省略)
            }
        },
        error: function (jqXHR, textStatus, errorThrown) {
            // エラーの場合の処置(省略)
        }
    });
}

Tags: ,

MVC

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  2018年9月  >>
2627282930311
2345678
9101112131415
16171819202122
23242526272829
30123456

View posts in large calendar