ASP.NET Web API 參數繫結

摘要:測試 Web API 參數繫結的幾種寫法,以及使用 WebApiContrib 套件中的 MvcStyleBinding 來解決 ASP.NET Web API 在繫結複雜型別的參數的限制:無法同時支援從 URI 查詢字串以及從 POST body  中取得參數值。

在示範各種參數繫結寫法之前,先列出我的 App_Start\WebApiConfig 的設定:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{action}/{id}",    // 加了 action, 如同 MVC
            defaults: new { id = RouteParameter.Optional }
        );

        // 預設傳回 JSON 格式.
        var appXmlType = config.Formatters.XmlFormatter.SupportedMediaTypes.FirstOrDefault(
            t => t.MediaType == "application/xml");
        config.Formatters.XmlFormatter.SupportedMediaTypes.Remove(appXmlType);
    }
}

然後在專案中新建立一個 Web API Controller,命名為 ModelBindingController:

public class ModelBindingController : ApiController
{
  // 底下示範的方法寫在這裡.
}

接著實驗各種傳入參數的寫法。為了避免太多圖片,執行結果大都以文字描述,比較特別的地方才抓圖。

Demo 1:簡單參數

對於簡單型別的參數,ASP.NET Web API 預設會從 URI 查詢字串取得參數值,並指定給動作方法中對應的參數--此程序叫做繫結(binding)。

[HttpGet]
public string Demo1(int id)
{
    return id.ToString();
}

測試:http://.../api/ModelBinding/Demo1/10
結果:"10"

測試:http://.../api/ModelBinding/Demo1?id=10
結果:"10"

測試:http://.../api/ModelBinding/Demo1
結果:
{"Message":"No HTTP resource was found that matches the request URI 'http://localhost:15712/api/ModelBinding/Demo1'.","MessageDetail":"No action was found on the controller 'ModelBinding' that matches the request."}

最後一個測試出錯,因為呼叫端沒有提供參數 id。

Demo2:可有可無的參數

[HttpGet]
public string Demo2(int? id = null)
{
    if (id == null)
    {
        return "id, please!";
    }
    return id.ToString();
}

測試:http://.../api/ModelBinding/Demo2?id=10
結果:"10"

測試:http://.../api/ModelBinding/Demo2
結果:"id, please!"

測試:用 Fiddler 送出 HTTP POST 請求,如下圖:


結果傳回如下錯誤訊息:
{"Message":"The requested resource does not support http method 'POST'."}

這是因為 Demo2() 僅接受 HTTP GET 請求的緣故。

Demo 3:同時接受 GET 和 POST

[HttpGet, HttpPost]
public string Demo3(int id)
{
    return id.ToString();
}

HTTP GET 測試:http://.../api/ModelBinding/Demo3?id=10
結果:"10"

HTTP POST 測試:使用 Fiddler。
結果:HTTP 404,錯誤訊息:
{"Message":"No HTTP resource was found that matches the request URI 'http://localhost:15712/api/ModelBinding/Demo3'.","MessageDetail":"No action was found on the controller 'ModelBinding' that matches the request."}

若在 Demo3() 的參數 id 前面加上 [FromBody],表示要從 HTTP POST 內文來取得參數值:

[HttpGet, HttpPost]
public string Demo3([FromBody]int id)
{
    return id.ToString();
}

此時再用 Fiddler 送出 POST 請求就能正確取得參數值:


注意圖中的 Request Headers 欄位必須加上「Content-Type: application/x-www-form-urlencoded」,以及 RequestBody 裡面的參數的寫法是「=100」;如果寫成「id=100」,雖然可呼叫成功(HTTP 200),可是 ASP.NET Web API 取到的參數值會是 0。

Demo 4:多個參數,GET 與 POST

使用 GET(URI 查詢字串)來接多個參數肯定沒問題,如果有的參數從 URI 取得,有的參數從 POST 內文取呢?

[HttpGet, HttpPost]
public string Demo4(int id, [FromBody]string companyName)
{
    return String.Format("ID: {0}, Company: {1}", id, companyName };
}

HTTP GET 測試:http://.../api/ModelBinding/Demo3?id=10&companyName=MacroSoft
結果:"ID: 10, Company: "
解釋:companyName 參數接不到,因為我們用 [FromBody] 限定它要從 POST 內文取得。

HTTP POST 測試:使用 Fiddler,在 URL 查詢字串指定 id=10,並於 Request Body 中輸入「=MacroSoft」。
結果:"ID: 10, Company: MacroSoft"

Demo 5:繫結複雜型別

前面都是繫結簡單型別,如整數、字串等。這裡開始繫結複雜型別。我用一個自訂的 Customer 類別來實驗:

public class Customer
{
    public int ID { get; set; }
    public string CompanyName { get; set; }

    public override string ToString()
    {
        return String.Format("ID: {0}, Company: {1}", ID, CompanyName);
    }
}

動作方法:

[HttpGet]
public string Demo5(Customer customer)
{
    return customer.ToString();
}

HTTP GET 測試:發生 HTTP 500 錯誤,訊息如下:
{"Message":"An error has occurred.","ExceptionMessage":"並未將物件參考設定為物件的執行個體。","ExceptionType":"System.NullReferenceException","StackTrace":"...略..."}

這是因為複雜型別的參數繫結預設會從 POST body 中取得。如果在參數前面加上 [FromUri]

[HttpGet]
public string Demo5([FromUri] Customer customer)
{
    return customer.ToString();
}

那麼先前的 HTTP GET 測試便能順利解析參數。

Demo 6:繫結複雜型別,使用 HTTP POST

[HttpPost]
public string Demo6(Customer customer)
{
    return customer.ToString();
}

使用 Fiddler 測試 HTTP POST,在 Request body 中填入「id=10&companyName=MacroSoft」,結果可以取得順利成功。

注意這次在 Fiddler 中的參數寫法跟先前 Demo3 測試 HTTP POST 時的「=100」寫法不同。這是因為 ASP.NET Web API 其實是把 POST body 裡面的內容當成一個參數,並嘗試繫結至你的動作方法所宣告的傳入參數。以這個例子來說,它會嘗試將字串 「id=10&companyName=MacroSoft」反序列化成 Customer 物件。

先前 Demo 3 那種在 POST body 中填入「=100」的語法,可用來繫結簡單參數,而且也只能繫結一個參數。

如果不想繫結至自訂的 model 類別(如本例的 Customer),也可以使用 FormDataCollection

Demo 7:繫結多個複雜型別

[HttpPost]
public string Demo7(Customer customer1, Customer customer2)
{
    return customer1.ToString() + ", " + customer2.ToString();
}

一樣用 Fiddler 測試,結果失敗:

{"Message":"An error has occurred.","ExceptionMessage":"Can't bind multiple parameters ('customer1' and 'customer2') to the request's content.","ExceptionType":"System.InvalidOperationException","StackTrace":null}

如先前所說,ASP.NET Web API 無法透過 POST body 來繫結兩個或兩個以上的參數(此個數係指 Web API 方法所宣告的傳入參數)。

那麼改用 [HttpGet] 並且在兩個參數前面都加上 [FromUri] 呢?也沒用。雖然執行時不會出錯,但解析出來的參數值不可能正確--當然了,兩個 id 參數要如何決定誰是誰?

Demo 8:繫結複雜型別,同時支援 GET 和 POST

[HttpGet, HttpPost]
public string Demo8(Customer customer)
{
    return customer.ToString();
}

這樣寫法,是希望我們的 API 能同時支援 HTTP GET 和 HTTP POST。可是行不通--只有 HTTP POST 能夠正確解析參數。

預設情況下,若要傳遞複雜型別,GET 和 POST 只能二選一。據說這是因為考慮到 ASP.NET Web API 非同步呼叫的本質,所以解析參數時只會讀取一次,而並未將這些資料緩存(buffer),故參數繫結的機會只有一次;就好像串流,一旦讀取之後,就不能回頭再讀一遍。

幸好已經有好心人幫我們寫好輔助工具,不用自己傷腦筋了。接下來的 Demo 9 會說明解決方法。

Demo 9: 使用 MVC 式參數繫結來同時支援 GET 和 POST 

WebApiContrib 裡面有個 MvcStyleBinding attribute,直接套用到我們的 API Controller 類別上,就能像 MVC Controller 那樣同時支援 URL 查詢字串和 POST body 中取得參數值。

使用方法很簡單:先用 NuGet 取得 WebApiContrib 套件並加入組件參考,然後在需要「MVC 式參數繫結」的 API Controller 類別中做兩件事:
  1. using WebApiContrib.ModelBinders;
  2. 為你的 API Controller 類別套用 attribute: [MvcStyleBinding]
  3. 為你的動作方法套用 [HttpGet, HttpPost]
像這樣:

using WebApiContrib.ModelBinders;

namespace WebApiDemo.Controllers.Api
{
    [MvcStyleBinding]
    public class ModelBindingController : ApiController
    {
        // 略.
    }
}

然後先前的 Demo8() 都不用修改,直接對它測試 HTTP GET 和 HTTP POST,結果都能順利解析參數。不僅如此,現在我們甚至可以在使用 HTTP POST 的情況下繫結多個參數。Cool!

小結

重點一:對於簡單型別的參數,ASP.NET Web API 預設會從 URI 查詢字串解析並繫結至動作方法的參數。對於複雜型別,則預設從 POST body 中取得參數值。

重點二:在解析 HTTP POST 參數的場合,ASP.NET Web API 其實是把 POST body 裡面的內容當成一個參數,並嘗試繫結至你的動作方法所宣告的傳入參數。簡單講就是 ASP.NET Web API 不支援從 POST body 繫結多個參數,此特性(限制)通常也會讓開發人員傾向採用 model building 來解決此問題(如本文範例中的 Customer 類別)。

WebApiContrib 的 MvcStyleBinding 不僅可以解決上述問題,也能夠讓我們像使用 MVC 參數繫結那樣,同時支援 HTTP GET 和 HTTP POST(參考 Demo 9 的範例)。

採用 model binding 時,可以繫結至我們自己寫的類別,或者也可以用 FormDataCollection。還有其他繫結參數的方法,例如使用 JObject,改天有空再實驗。先醬~

延伸閱讀

5 則留言:

  1. 謝謝版主分享,很受用的文章;另外想請教,若是提供服務給別人使用,是否有甚麼建議的身份驗證機制,限制只有得到認可人才可以存取?

    回覆刪除
  2. 考慮到 REST 特性,Session 看來是不適合了。我看到有人自己做帳密驗證,驗證完後產生一組 token 或 access ID 給用戶端。以後用戶端每次呼叫都得傳入這個 token。這裡有篇文章我還沒讀,看似有關:http://sixgun.wordpress.com/2012/02/29/asp-net-web-api-basic-authentication/

    回覆刪除
  3. 多謝老師分享,很有用且好懂的資訊
    只是我很納悶,既然MVC實作RESTful WEBAPI的彈性這麼大
    微軟又何必發明另一套OData來擴充RESTful的查詢彈性

    回覆刪除
    回覆
    1. Hello!
      OData 是一種應用程式層次的協定,其主要用途是設計 CRUD 類型的 REST 服務。換句話說,如果你的用戶端應用程式是 HTTP-based,且大多是針對一般資料進行 CRUD 操作,便可以考慮採用 OData。

      相較於 OData 特別著重資料操作,ASP.NET Web API 則是更通用、更彈性的技術。雖然都是基於 HTTP,兩者並非互斥,而是可以一起搭配使用的。也就是說,我們可以在 Web API 中建立 OData 端點。如果看一下 OData 對於資料操作的定義與呼叫格式,可能更能了解兩種技術之間的差異。這裡有篇文章可以參考:http://www.dotblogs.com.tw/joysdw12/archive/2013/06/07/web-api-odata.aspx

      此外,目前 OData 已經是 OASIS 標準之一。不只 .NET 支援 OData,連 Java、C++ 都有函式庫喔。

      刪除
    2. 原來如此,多謝老師精闢的解釋^^

      刪除

技術提供:Blogger.
回頂端⬆️