ASP.NET Web API 心得筆記 (1)

ASP.NET Web API 是一個框架(framework),能讓你在 .NET Framwork 之上架設 HTTP 服務 (HTTP Services)。ASP.NET Web API 是 .NET Framework 上建置 RESTful 應用程式的理想平台。

在 Julie Lerman's 的 How I see Web API 一文中,用了一張圖來簡明說明 Web API:

比較 WCF 與 Web API
圖一:比較 WCF 與 Web API from thedatafarm.com
註:以下內容,會使用 Visual Studio 11 Beta 來實作,因 VS 11 還未有正體中文版,與 VS 11 相關步驟會使用英文。另外,可順便先熟悉 VS 11 開發環境。除簡化的圖示不談,整體開發功能是更上一層樓。

建立 Web API 專案


開啟 VS 11 → Start → New Project → Installed → Templates → Visual Basic → Web → ASP.NET MVC 4 Web Application → 在 Name 輸入 "HelloWebAPI" → OK

選擇 ASP.NET MVC 4 專案
圖二:選擇 ASP.NET MVC 4 專案
在 New ASP.NET MVC 4 Project 裡選擇 Web API 然後點擊 OK。

選擇 Web API 樣版
圖三:選擇 Web API 樣版
這裡有個有趣的地方。
之前,我在接觸 ASP.NET MVC 3 時,有點想去學 Razor 語法,因為大家一直強調 Razor 的好處,也讓我小小心動,不過,每一次接觸這種 v1 東西總是讓我碰壁,在 Visual Studio 2010 + Visual Basic 2010 + MVC 3 + Razor 環境裡,Visual Studio 2010 當的不像話,隨便打個 @html. 就可以當好幾次,後來還是把心思放在 MVC 上就好。但在 Visual Studio 11 裡中,VB中 Web API 只的支援 Razor,資料沒看錯的話,Visual Studio 11中 是 Razor v2。目前還沒有去 View 裡認真玩,簡單打一些 keyword 是沒有之前當掉情況,未來有 VB for Razor 的心得,再寫上來。

加入 Model

Model 是在應用程式中使用物件(object)方式來表現資料。ASP.NET Web API 能自己序列化(serialize) 你的 Model 成為 JSON、XML 或其他資料格式,然後寫入這些已序列化資料到 HTTP 回應訊息 (response message) 的主體 (body) 中。只要用戶端 (client) 能讀取序列化格式,它就能反解析序列化成為物件。許多用戶端都能同時解析 XML 與 JSON。而且,用戶端可以在 HTTP 請求訊息 (request message)的 Accept header 裡直接設定那種格式 (JSON, XML, Other) 是要接受的。

有沒有很心動?我們先加入一個 Products Model。



新增 Products Model
圖四:新增 Products Model
類別名稱:Products

''' <summary>
''' 產品模型物件
''' </summary>
Public Class Products
    Property Id As Integer
    Property Name As String
    Property Price As Decimal
End Class

加入 Controller

controller 是在處理從用戶端來的 HTTP 請求的物件。預設 ASP.NET Web API 專案會建置兩個 controller。
  • HomeController 是傳統 ASP.NET MVC 的 controller。它與我們 Web API 服務沒有直接關係。
  • ValuesController 是一個 Web API controller 範例。
注意 如果你已經有在運作的 ASP.NET MVC,它也有類似的 controllers,它們的運作類似 Web API,但 Web API 源自於 ApiController 類別來替代 Controller 類別。最主要的差異處在於 action 上,Web API controllers裡的 action 不會回傳view,它們回傳資料(data)
先將 ValuesController 改名。

修改 Controller 名稱
圖五:修改 Controller 名稱
改名稱為 "ProductsController.vb",然後會有提醒視窗:

是否同步修改有參考的程式碼
圖六:是否同步修改有參考的程式碼
我們簡單看一下 ProductsController.vb 的內容,

Imports System.Web.Http

Public Class ProductsController
    Inherits ApiController

    ' … 略 …
End Class

  1. 引用 System.Web.Http
  2. 繼承 ApiController 類別

只要是繼承 System.Web.Http.ApiController 類別,都會辨識為 Web API 的類別。我們把裡面所有預設 Action 全部註解,新增以下兩個方法:

Imports System.Web.Http

Public Class ProductsController
    Inherits ApiController

    ' GET /api/Products
    ''' <summary>
    ''' 取得所有產品列表
    ''' </summary>
    ''' <returns>所有產品列表</returns>
    Public Function GetAllProducts() As IEnumerable(Of Products)
        Return New List(Of Products) From {
            New Products() With {.Id = 1, .Name = "Cookie 1", .Price = 1.99D},
            New Products() With {.Id = 2, .Name = "Cookie 2", .Price = 2.99D},
            New Products() With {.Id = 3, .Name = "Cookie 3", .Price = 3.99D}}
    End Function

    ' GET /api/Products/2
    ''' <summary>
    ''' 取得指定 id 的產品資訊
    ''' </summary>
    ''' <param name="id">產品編號</param>
    ''' <returns>Products 物件</returns>
    Public Function GetProductById(id As Integer) As Products
        If id < 1 OrElse id > 3 Then
            Throw New HttpResponseException(System.Net.HttpStatusCode.NotFound)
        End If
        Return New Products() With {.Id = id,
                                    .Name = "Cookie " & id.ToString(),
                                    .Price = id + 0.99D}
    End Function
End Class

我們提供了兩個 Action,記得,在 Web API 裡我們要回傳的是資料(Data)而不是結果(ActionResult)。第一個 GetAllProducts() 的回傳型別為 IEnumerable(Of Products) 泛型,之後我們就能回傳任何有實作 IEnumerable(Of T) 的型別。第二個 GetProductById,傳入一個 id 值,先進行簡單判斷,如有值域有問題,就產生例外 HttpResponseException,此例外會產生 404 (Not Found) 錯誤。

就這樣,你已經擁有了一個可以運作的 HTTP Service。接下來讓 Client 來存取此 HTTP Service。

使用 Javascript 和 jQuery 呼叫 HTTP Service

在 HomeController.vb 裡新增一個 Action:

Function Product() As ActionResult
    Return View()
End Function

然後新增 Product.vbhtml 的 View ( 或按【Ctrl+M, Ctrl+V】):
新增 Product Action 的 View
圖七:新增 Product Action 的 View
新增 View 視窗
圖八:新增 View 視窗
View 部份,還是能自由選擇新增【ASPX(VB)】【Razor(VBHTML)】,我們選【Razor(VBHTML)】,寫入下列程式碼:

@Code
    ViewData("Title") = "Product"
    Layout = "~/Views/Shared/_Layout.vbhtml"
End Code
<div>
    <h1>所有產品</h1>
    <ul id='products' />
</div>
<div>
    <label for="prodId">ID:</label>
    <input type="text" id="prodId" size="5" />
    <input type="button" value="Search" onclick="find();" />
    <p id="product" />
</div>

取得所有產品列表資料


這裡,我們透過 jQuery傳送 HTTP GET 請求到 "/api/products" 這一個 HTTP Service 來取得所有產品列表。

<script type="text/javascript">
    $(function(){
        // 傳送 AJAX 請求
        $.getJSON("/api/Products/",
        function (data) {
            // 成功, data 會包含所有產品列表
            $.each(data, function (key, val) {
                // 格式化文字資料,以方便顯示
                var str = val.Name + ': $' + val.Price;

                // 將產品資料建置成 li項目,然後加入 ul 元素中
                $('<li/>', { html: str }).appendTo($('#products'));   
            });
        });
    });
</script>

透過 getJSON 來傳送 AJAX 請求,然後 HTTP Service 會回應一個 JSON 陣列物件。第二個參考為回呼函式,當呼叫(invoked)成功時,會被執行。

透過 ID 取得產品

這裡,我們透過 jQuery傳送 HTTP GET 請求到 "/api/products/id" 這一個 HTTP Service 來取得產品資料。

function find() {
    // 取的輸入的id
    var id = $('#prodId').val();

    // 傳送 AJAX 請求
    $.getJSON("/api/products/" + id,
    function (data) {
        // 成功
        var str = data.Name + ': $' + data.Price;
        $('#product').html(str);
    })
    .fail( // 失敗
    function (jqXHR, textStatus, err) {
        $('#product').html('Error: ' + err); 
    });
}

此一 find() 方法,一樣會傳送 AJAX 請求,但它會傳送 id 參數給 HTTP Service,AJAX 請求成功與失敗都會顯示結果在畫面上。

最後完成 Product.vbhtml 完成程式碼:

@Code
    ViewData("Title") = "Product"
    Layout = "~/Views/Shared/_Layout.vbhtml"
End Code
<div>
    <h1>所有產品</h1>
    <ul id='products' />
</div>
<div>
    <label for="prodId">ID:</label>
    <input type="text" id="prodId" size="5" />
    <input type="button" value="Search" onclick="find();" />
    <p id="product" />
</div>
<script type="text/javascript">
    $(function(){
        // 傳送 AJAX 請求
        $.getJSON("/api/Products/",
        function (data) {
            // 成功, data 會包含所有產品列表
            $.each(data, function (key, val) {
                // 格式化文字資料,以方便顯示
                var str = val.Name + ': $' + val.Price;

                // 將產品資料建置成 li項目,然後加入 ul 元素中
                $('<li/>', { html: str }).appendTo($('#products'));   
            });
        });
    });
 
    function find() {
        // 取的輸入的id
        var id = $('#prodId').val();

        // 傳送 AJAX 請求
        $.getJSON("/api/products/" + id,
        function (data) {
            // 成功
            var str = data.Name + ': $' + data.Price;
            $('#product').html(str);
        })
        .fail( // 失敗
        function (jqXHR, textStatus, err) {
            $('#product').html('Error: ' + err); 
        });
    }
</script>

執行 Web API 應用程式


執行前,我介紹一個 Visual Studio 11 很棒的改進,就是我們在除錯時,可以隨時切換到任何我們有安裝的瀏覽器來都從瀏覽除錯的工作。

快速切換瀏覽器
圖九:快速切換瀏覽器
以前,我們還需要安裝一些外掛工具才能有以上功能,設定你想使用的瀏覽器,然後按【F5】或直接按【黑三角型】,即可開啟設定的瀏覽器。裡面還有一個超棒的【Page Inspector, 網頁巡覽者】,改天再來介紹。我們使用【Internet Explorer】來進行執行 Web API 應用程式的工作。

執行預設網站,開啟預設網頁後,我們在【網址列】輸入【http://localhost:7155/home/product】(Port 會不同) 的路由,我們會得到以下畫面:

執行 Web API 應用程式畫面
圖十:執行 Web API 應用程式畫面
我們讀入產品資料時,由於使用 AJAX 請求,所以畫面不會重新整理。接下來看搜尋結果:

使用 Id 搜尋產品資料
圖十一:使用 Id 搜尋產品資料
使用不合法字元搜尋產品資料
圖十二:使用不合法字元搜尋產品資料

使用 F12 開發者工具查看 HTTP 請求與回應


我在一開始執行 IE 時,已經按下【F12 開發者工具】,然去【網路】→【開始擷取】→回 IE 按【F5】,去擷取我所執行的動作,

Web API 執行流程詳細資料
圖十三:Web API 執行流程詳細資料
注意我們的 "/api/Products/" 等 HTTP Service 請求,在類型很明顯是回應 "application/json",點擊【移至詳細檢視】:

要求標頭
圖十四:要求標頭
回憶一下,我們一開頭所提的,Clinet 可以要求想要的格式。

回應本文
圖十五:回應本文
再回想一下上面的 Web API 程式裡,你有寫任何與 JSON 有關的程式碼嗎?

最後,我們討論一下 Web API 的 Routing

談到 Routing 就不能不看 Global.asax,我們能發現,Web API 專案裡的 Global.asax 有些許不同。

這是 MVC 3 專案的 Global.asax

' 注意: 如需啟用 IIS6 或 IIS7 傳統模式的說明,
' 請造訪 http://go.microsoft.com/?LinkId=9394802

Public Class MvcApplication
    Inherits System.Web.HttpApplication

    Shared Sub RegisterGlobalFilters(ByVal filters As GlobalFilterCollection)
        filters.Add(New HandleErrorAttribute())
    End Sub

    Shared Sub RegisterRoutes(ByVal routes As RouteCollection)
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}")

        ' MapRoute 接受下列參數 (按順序):
        ' (1) 路由名稱
        ' (2) URL 及參數
        ' (3) 參數預設值
        routes.MapRoute( _
            "Default", _
            "{controller}/{action}/{id}", _
            New With {.controller = "Blog", .action = "Index", .id = UrlParameter.Optional} _
        )

    End Sub

    Sub Application_Start()
        AreaRegistration.RegisterAllAreas()

        RegisterGlobalFilters(GlobalFilters.Filters)
        RegisterRoutes(RouteTable.Routes)
    End Sub
End Class

這是 MVC 4 Web API 專案的 Global.asax

' Note: For instructions on enabling IIS6 or IIS7 classic mode, 
' visit http://go.microsoft.com/?LinkId=9394802
Imports System.Data.Entity
Imports System.Data.Entity.Infrastructure
Imports System.Web.Http
Imports System.Web.Optimization

Public Class WebApiApplication
    Inherits System.Web.HttpApplication

    Shared Sub RegisterGlobalFilters(ByVal filters As GlobalFilterCollection)
        filters.Add(New HandleErrorAttribute())
    End Sub

    Shared Sub RegisterRoutes(ByVal routes As RouteCollection)
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}")

        routes.MapHttpRoute( _
            name:="DefaultApi", _
            routeTemplate:="api/{controller}/{id}", _
            defaults:=New With {.id = RouteParameter.Optional} _
        )

        routes.MapRoute( _
            name:="Default", _
            url:="{controller}/{action}/{id}", _
            defaults:=New With {.controller = "Home", .action = "Index", .id = UrlParameter.Optional} _
        )
    End Sub

    Sub Application_Start()
        AreaRegistration.RegisterAllAreas()

        ' Use LocalDB for Entity Framework by default
        Database.DefaultConnectionFactory = New SqlConnectionFactory("Data Source=(localdb)\v11.0; Integrated Security=True; MultipleActiveResultSets=True")

        RegisterGlobalFilters(GlobalFilters.Filters)
        RegisterRoutes(RouteTable.Routes)

        BundleTable.Bundles.RegisterTemplateBundles()
    End Sub
End Class

差異算不小,在 MVC 4 Web API 專案的 Global.asax 中:

  1. 預設引用的命名空間變多了
  2. 預設 Routing 規則為兩條,一條為  Web API 使用,一條為預設 Routing 使用
  3. 指定變數方式改為,改為指名變數(:=)方式指定
  4. Application_Start() 預設使用 LocalDB 而非 SQL Server Express,預設啟用 Bundling 和 Minification 功能。

這裡我專注討論 Web API Routing內容,我們來看 Routing 規則:

routes.MapHttpRoute( _
    name:="DefaultApi", _
    routeTemplate:="api/{controller}/{id}", _
    defaults:=New With {.id = RouteParameter.Optional} _
)

在 Web API Routing 中,我們注意到,有新的 MapHttpRoute() 延伸方法,它使用路由樣版 "api/{controller}/{id}",我們發現第一件事,沒有 {action} 了。我們前面提過,Web API 是要提供類似 WCF 的服務,而這裡的判斷改使用 HTTP 的傳輸方法( GET / POST / PUT / DELETE ...),如果你有注意一開始 ValuesController.vb 的內容:

' GET /api/default2
Public Function GetValues() As IEnumerable(Of String)
    Return New String() {"value1", "value2"}
End Function

' GET /api/default2/5
Public Function GetValue(ByVal id As Integer) As String
    Return "value"
End Function

' POST /api/default2
Public Sub PostValue(ByVal value As String)

End Sub

' PUT /api/default2/5
Public Sub PutValue(ByVal id As Integer, ByVal value As String)

End Sub

' DELETE /api/default2/5
Public Sub DeleteValue(ByVal id As Integer)

End Sub

剛好對應到 HTTP 的傳輸方法,所以就無需再設定 {action} 了。那我要如何決定要使用那個 Action 呢?回頭看一下【圖十三】,你發出為 GET 請求,那 Routing 就會去找 {controller} 裡 GET 開頭的 Action,這裡也讓我了解到一點,Web API 的 Action 命名請小心,必須使用 Get、Post、Put、Delete 等開頭。預設有二個 Get 的 Action,一個無參數,一個有參數,再看【圖十三】:

  • GET /api/Products/ → GetAllProducts()
  • GET /api/Products/2 → GetProductById(id As Integer)
  • GET /api/Products/adsf → GetProductById(id As Integer)
很明顯了吧,第一個找無參數 Get 方法,第二、三個找有參數 Get 方法。看到此,應該就能了解,Web API 裡的 Routing 流程。

參考資料

3 則留言:

  1. 很完整的經驗分享,也因為這篇文章而有學習的機會。
    但是有一個小缺點是在 Product.vbhtml 的撰寫過程中,
    似乎遺漏將 jquery 匯入的這段程式碼,僅提出這項建議。

    回覆刪除
  2. 您好,圖一的對照圖似乎死了。

    回覆刪除
  3. 原網站改版,可不知跑到那裡去了…

    回覆刪除

感謝您的留言,如果我的文章你喜歡或對你有幫助,按個「讚」或「分享」它,我會很高興的。