摘要:測試 Web API 參數繫結的幾種寫法,以及使用 WebApiContrib 套件中的 MvcStyleBinding 來解決 ASP.NET Web API 在繫結複雜型別的參數的限制:無法同時支援從 URI 查詢字串以及從 POST body 中取得參數值。
在示範各種參數繫結寫法之前,先列出我的 App_Start\WebApiConfig 的設定:
然後在專案中新建立一個 Web API Controller,命名為 ModelBindingController:
接著實驗各種傳入參數的寫法。為了避免太多圖片,執行結果大都以文字描述,比較特別的地方才抓圖。
Demo 1:簡單參數
對於簡單型別的參數,ASP.NET Web API 預設會從 URI 查詢字串取得參數值,並指定給動作方法中對應的參數--此程序叫做繫結(binding)。
測試: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:可有可無的參數
測試: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
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 內文來取得參數值:
此時再用 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 內文取呢?
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 類別來實驗:
動作方法:
HTTP GET 測試:發生 HTTP 500 錯誤,訊息如下:
{"Message":"An error has occurred.","ExceptionMessage":"並未將物件參考設定為物件的執行個體。","ExceptionType":"System.NullReferenceException","StackTrace":"...略..."}
這是因為複雜型別的參數繫結預設會從 POST body 中取得。如果在參數前面加上 [FromUri]:
那麼先前的 HTTP GET 測試便能順利解析參數。
Demo 6:繫結複雜型別,使用 HTTP POST
使用 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:繫結多個複雜型別
一樣用 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
這樣寫法,是希望我們的 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 類別中做兩件事:
然後先前的 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,改天有空再實驗。先醬~
延伸閱讀
在示範各種參數繫結寫法之前,先列出我的 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."}
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 類別中做兩件事:
- using WebApiContrib.ModelBinders;
- 為你的 API Controller 類別套用 attribute: [MvcStyleBinding]
- 為你的動作方法套用 [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,改天有空再實驗。先醬~
延伸閱讀
- How WebAPI does Parameter Binding by Mike Stall, 16 Apr 2012
- MVC Style parameter binding for WebAPI
謝謝版主分享,很受用的文章;另外想請教,若是提供服務給別人使用,是否有甚麼建議的身份驗證機制,限制只有得到認可人才可以存取?
回覆刪除考慮到 REST 特性,Session 看來是不適合了。我看到有人自己做帳密驗證,驗證完後產生一組 token 或 access ID 給用戶端。以後用戶端每次呼叫都得傳入這個 token。這裡有篇文章我還沒讀,看似有關:http://sixgun.wordpress.com/2012/02/29/asp-net-web-api-basic-authentication/
回覆刪除多謝老師分享,很有用且好懂的資訊
回覆刪除只是我很納悶,既然MVC實作RESTful WEBAPI的彈性這麼大
微軟又何必發明另一套OData來擴充RESTful的查詢彈性
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++ 都有函式庫喔。
原來如此,多謝老師精闢的解釋^^
刪除