[ASP.NET MVC 小牛之路]18 - Web API

Liam Wang發表於2014-10-19

Web API 是ASP.NET平臺新加的一個特性,它可以簡單快速地建立Web服務為HTTP客戶端提供API。Web API 使用的基礎庫是和一般的MVC框架一樣的,但Web API並不是MVC框架的一部分,微軟把Web API相關的類從 System.Web.Mvc 名稱空間下提取了出來放在 System.Web.Http 名稱空間下。這種理念是把 Web API 作為ASP.NET 平臺的核心之一,以使Web API能使用在其他的Web應用中,或作為一個獨立的服務引擎。本文將先帶大家理解Web API,再教大家在MVC中使用Web API。

本文目錄

理解 REST 和 RESTful Web API

為了更好的理解Web API,先帶大家瞭解一下 REST 和 RESTful Web API。以下內容大多來自維基百科

REST(全名:Representational State Transfer),中文翻譯是表徵狀態轉移(也有叫表述性狀態轉移),Roy Fielding博士在2000年他的博士論文中提出來的一種軟體架構風格。

REST 從資源的角度來觀察整個網路,分佈在各處的資源由URI確定,而客戶端的應用通過URI來獲取資源的表徵。獲得這些表徵致使這些應用程式轉變了其狀態。隨著不斷獲取資源的表徵,客戶端應用不斷地在轉變著其狀態,所謂表徵狀態轉移(Representational State Transfer)。

目前使用Web服務的三種主流的方式是:遠端過程呼叫(RPC),面向服務架構(SOA)以及表徵性狀態轉移(REST),其中REST模式的Web服務與複雜的SOARPC對比來講顯的更加簡潔,越來越多的web服務開始採用REST風格設計和實現。

需要注意的是,REST是設計風格而不是標準,但REST設計風格常基於使用HTTPURI,和XML以及HTML這些現有的廣泛流行的協議和標準。REST設計風格有如下要點:

  • 資源是由URI來指定。
  • 對資源的操作包括獲取、建立、修改和刪除資源,這些操作正好對應HTTP協議提供的GET、POST、PUT和DELETE方法。
  • 通過操作資源的表現形式來操作資源。
  • 資源的表現形式則是XML或HTML,取決於讀者是機器還是人,是消費web服務的客戶軟體還是web瀏覽器。當然也可以是任何其他的格式,如JSON。

另外,使用REST需要滿足一些要求,如客戶端和伺服器結構、連線協議具有無狀態性、能夠利用Cache機制增進效能等。

RESTful Web API(也稱為RESTful Web服務)是一個使用HTTP並遵循REST原則的Web服務。它從以下請求資源的三個方面進行定義:

  • URI,比如:http://example.com/resources/。
  • Web服務接受與返回的網際網路媒體型別,比如:JSON,XML ,YAML 等。
  • Web服務在該資源上所支援的一系列請求方法(比如:POST,GET,PUT或DELETE)。

本文要講的ASP.NET Web API 就是RESTful Web API的一種。下表列出了在實現 RESTful Web API 時HTTP請求方法的典型用途:

不像基於SOAP的Web服務,RESTful Web服務並沒有“正式”的標準。這是因為REST是一種架構,而SOAP只是一個協議。雖然REST不是一個標準,但在實現RESTful Web服務時可以使用其他各種標準(比如HTTP,URL,XML,PNG等)。

那麼REST和本文要講的ASP.NET API又有什麼關係呢?請繼續往下閱讀。

理解 ASP.NET Web API

ASP.NET Web API(本文簡稱Web API),是基於ASP.NET平臺構建RESTful應用程式的框架。可以說 Web API 就是為在.NET平臺下構建RESTful應用程式而生的,這也是本文要先介紹REST的原因。

Web API基於在 MVC 應用程式中新增的一個特殊的 Controller,這種 Controller 稱為 API Controller,和MVC普通的 Controller 相比它主要有如下兩個不同的特點:

  1. Action 方法返回的是 Model 物件,而不是ActionResult。
  2. 在請求時,Action 方法是基於 HTTP 請求方式來選擇的。

第一個不同點很好理解,第二個不同點可能讀者不太理解,一會看完本文的示例就理解了。

從API Controller的Action方法返回給客戶端的Model物件是經過JSON編碼的。API Controller的設計僅是為了提供傳遞Web資料的服務,因此它不支援View、Layout 和其它HTML呈現相關的特性。Web API 能支援任何有Web 功能的客戶端,但最常用的是為Web應用程式中的Ajax請求提供服務。

正如在 ASP.NET MVC 小牛之路]14 - Unobtrusive Ajax 中演示的,我們也可以通過普通的Controller中建立Action方法來返回JSON格式的資料來支援Ajax,但 API controller 的方式可以使得資料相關的Action和View相關的Action分開,並且使得建立只為資料服務的應用更快更簡單。

一般我們會在下面這兩種情況下選擇使用API Controler:

  1. 需要大量的返回JSON格式資料的Action方法。
  2. 和HTML無關,只是純粹為資料提供服務。

如果你對上面這些概念還不太理解,沒關係,當你閱讀完本文後再回頭看一下,你會對這些概念理解得更深些。

下面我會通過例子來解釋Web API是如何工作的,它非常簡單,因為很多東西都和我們之前講過的MVC的東西相同。

建立 Web API 應用程式

作為本文的示例,我們建立一個名為 WebServices 的MVC應用程式,選擇Web API模板。在本文的這個例子中,我們建立一個名為 Reservation 的Model,程式碼如下:

namespace WebServices.Models {
    public class Reservation {
        public int ReservationId { get; set; }
        public string ClientName { get; set; }
        public string Location { get; set; }
    }
}

為這個Model建立一個Repository 介面和它的一個簡單的實現。如果你對 Repository 這個詞不太理解,可以閱讀:[ASP.NET MVC 小牛之路]05 - 使用 Ninject 。為了簡單,我們直接在Models資料夾中新增一個名為 IReservationRepository 的介面,程式碼如下:

namespace WebServices.Models {
    public interface IReservationRepository {
        IEnumerable<Reservation> GetAll();
        Reservation Get(int id);
        Reservation Add(Reservation item);
        void Remove(int id);
        bool Update(Reservation item);
    }
}

建立一個名為 ReservationRepository 的類,實現 IReservationRepository 介面,程式碼如下:

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Web; 
 
namespace WebServices.Models { 
    public class ReservationRepository : IReservationRepository { 
        private List<Reservation> data = new List<Reservation> { 
            new Reservation {ReservationId = 1,  ClientName = "Adam",  
                Location = "London"}, 
            new Reservation {ReservationId = 2,  ClientName = "Steve",  
                Location = "New York"},new Reservation {ReservationId = 3,  ClientName = "Jacqui",  
                Location = "Paris"}, 
        };

        private static ReservationRepository repo = new ReservationRepository();
        public static IReservationRepository getRepository() {
            return repo;
        }

        public IEnumerable<Reservation> GetAll() {
            return data;
        }

        public Reservation Get(int id) {
            var matches = data.Where(r => r.ReservationId == id);
            return matches.Count() > 0 ? matches.First() : null;
        }

        public Reservation Add(Reservation item) {
            item.ReservationId = data.Count + 1;
            data.Add(item);
            return item;
        }

        public void Remove(int id) {
            Reservation item = Get(id);
            if (item != null) {
                data.Remove(item);
            }
        }

        public bool Update(Reservation item) {
            Reservation storedItem = Get(item.ReservationId);
            if (storedItem != null) {
                storedItem.ClientName = item.ClientName;
                storedItem.Location = item.Location;
                return true;
            } else {
                return false;
            }
        }
    }
}
ReservationRepository

簡單起見,我們這裡沒有真正實現資料持久化,只是簡單的模擬。

在我們建立好Web API應用程式時,VS已經新增好了一個預設的HomeController和Index.cshtml View。我們不打算在HomeControllerr 的Action中傳遞Model給View,因為一會要在View中使用JavaScript呼叫Web API來獲取所有資料。在一個MVC工程中,你可以自由混合地使用普通Controller和API Controller。

修改 Index.cshtml 如下:

@{ ViewBag.Title = "Index";} 
@section scripts { 
    <script src="~/Scripts/jquery.unobtrusive-ajax.js"></script> 
} 
<div id="summaryDisplay" class="display"> 
    <h4>Reservations</h4> 
    <table> 
        <thead> 
            <tr> 
                <th class="selectCol"></th> 
                <th class="nameCol">Name</th> 
                <th class="locationCol">Location</th> 
            </tr>
            </thead> 
        <tbody id="tableBody"> 
            <tr><td colspan="3">The data is loading</td></tr> 
        </tbody> 
    </table> 
    <div id="buttonContainer"> 
        <button id="refresh">Refresh</button> 
        <button id="add">Add</button> 
        <button id="edit">Edit</button> 
        <button id="delete">Delete</button> 
    </div> 
</div> 
 
<div id="addDisplay" class="display"> 
    <h4>Add New Reservation</h4> 
    @{ 
        AjaxOptions addAjaxOpts = new AjaxOptions { 
            // options will go here 
        }; 
    } 
    @using (Ajax.BeginForm(addAjaxOpts)) { 
        @Html.Hidden("ReservationId", 0) 
        <p><label>Name:</label>@Html.Editor("ClientName")</p> 
        <p><label>Location:</label>@Html.Editor("Location")</p> 
        <button type="submit">Submit</button> 
    } 
</div> 
 
<div id="editDisplay" class="display"> 
    <h4>Edit Reservation</h4> 
    <form id="editForm"> 
        <input id="editReservationId" type="hidden" name="ReservationId"/> 
        <p><label>Name:</label><input id="editClientName" name="ClientName" /></p> 
        <p><label>Location:</label><input id="editLocation" name="Location" /></p> 
    </form> 
    <button id="submitEdit" type="submit">Save</button> 
</div>
Index.cshtml

並把 _Layout.cshtml 中的一些沒用的內容清理一下,刪除 /Content/Site.css 中的樣式後新增如下樣式程式碼:

table { margin: 10px 0;} 
th { text-align: left;} 
.nameCol {width: 100px;} 
.locationCol {width: 100px;} 
.selectCol {width: 30px;} 
.display { float: left; border: thin solid black; margin: 10px; padding: 10px;} 
.display label {display: inline-block;width: 100px;}
CSS

到這,我們的程式執行起來是這樣的:

接下來我們需要建立一個API Controller,通過它我們可以用JavaScript程式碼和Repository的內容進行互動。

右擊  Controllers 資料夾,選擇“新增”--"控制器",在彈出的對話方塊中修改Controller的名稱為 ReservationController,並從模板中選擇 Empty API。新增好後,修改 ReservationController 如下:

using System.Collections.Generic;
using System.Web.Http;
using WebServices.Models;

namespace WebServices.Controllers {
    public class ReservationController : ApiController {
        IReservationRepository repo = ReservationRepository.getRepository();

        public IEnumerable<Reservation> GetAllReservations() {
            return repo.GetAll();
        }

        public Reservation GetReservation(int id) {
            return repo.Get(id);
        }

        public Reservation PostReservation(Reservation item) {
            return repo.Add(item);
        }

        public bool PutReservation(Reservation item) {
            return repo.Update(item);
        }

        public void DeleteReservation(int id) {
            repo.Remove(id);
        }
    }
}
ReservationController

ReservationController的基類是  System.Web.Http.ApiController,也實現了 IController 介面(在[ASP.NET MVC 小牛之路]09 - Controller 和 Action (1)中有介紹),它和普通Controller的基類System.Web.Mvc.Controller 處理請求的方式基本上是一樣的。

到這我們就已經建立好了一個Web API了。

測試 API Controller

我們先來看看建立好的 API Controller 能否工作,下文將通過 API Controller 對資料的處理結果來解釋API Controller 如何工作。

執行程式,URL定位到 /api/reservation。你看到的結果將依賴於你所使用的版本,如果你使用的是IE 10/11,會彈出一個提示儲存檔案的對話方塊,該檔案的內容是以下JSON資料:

[{"ReservationId":1,"ClientName":"Adam","Location":"London"}, 
 {"ReservationId":2,"ClientName":"Steve","Location":"New York"}, 
 {"ReservationId":3,"ClientName":"Jacqui","Location":"Paris"}]

如果你使用的是另外一種瀏覽器,如Chrome或Firefox,瀏覽器將顯示如下XML資料:

<ArrayOfReservation> 
    <Reservation> 
        <ClientName>Adam</ClientName> 
        <Location>London</Location> 
        <ReservationId>1</ReservationId> 
    </Reservation> 
    <Reservation> 
        <ClientName>Steve</ClientName> 
       <Location>New York</Location> 
       <ReservationId>2</ReservationId> 
    </Reservation> 
    <Reservation> 
        <ClientName>Jacqui</ClientName> 
        <Location>Paris</Location> 
        <ReservationId>3</ReservationId> 
    </Reservation> 
</ArrayOfReservation>

對於看到的結果,我們有兩點感興趣。第一,我們請求 /api/reservation URL時,伺服器返回了Model物件的列表,根據該列表的資料,我們可以推斷呼叫的是ReservationController中的 GetAllReservations Action方法。第二,不同的瀏覽器接收到了不同格式的資料,我們可以猜測是由於不同版本的瀏覽器傳送請求的方式不一樣。

實際上,之所以會有不同格式的資料結果,是因為 Web API 使用HTTP請求報文頭部的Accept資訊來判斷客戶端更願意接收何種型別的資料。IE 接收到JSON格式的資料是因為它傳送的Accept頭部資訊是:

...
Accept: text/html, application/xhtml+xml, */* 
...

瀏覽器通過這段報文資訊告訴伺服器它最想要的是 text/html 內容,其次是 application/xhtml+xml。最後的  */* 意思是如果前兩種不滿足,就接收任何型別的資料。

Web API 支援JSON和XML,但它會優先選擇JSON格式。即IE的傳送Accept資訊中的 */* 使得Web API生成了JSON格式的資料。下面是 Chrome 瀏覽器傳送的Accept頭部資訊:

... 
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 
...

這段資訊的 application/xml 比 */* 優先順序高,所以Web API選擇為Chrome瀏覽器返回XML格式的資料。

對於Web服務,JSON已經開始大幅度替代XML了,因為XML冗長難以處理,尤其是在JavaScript中。

API Controller 如何工作

通過觀察Web API返回結果的資料,我們似乎有點理解API Controller是如何工作的了。如果你再將請求URL改為 /api/reservation/3,你將看到這樣的JSON資料(使用IE):

{"ReservationId":3,"ClientName":"Jacqui","Location":"Paris"}

這時,你可能更加明白了什麼。這時返回的資料是 ReservationId 為 3 的Reservation物件,我們可以推測Web API根據請求的URL(/api/reservation/3)呼叫的是GetReservation這個Action方法。這讓我們想到了之前講過的路由知識[ASP.NET MVC 小牛之路]07 - URL Routing

API Controller 在 /App_Start/WebApiConfig.cs 中有它們自己的路由配置,你可以開啟該檔案看看VS預設註冊好的路由:

public static void Register(HttpConfiguration config) {
    config.Routes.MapHttpRoute(
        name: "DefaultApi",
        routeTemplate: "api/{controller}/{id}",
        defaults: new { id = RouteParameter.Optional }
    );
}

API Controller使用的路由配置(WebApiConfig.cs)和普通MVC使用的配置(RouteConfig.cs)是在兩個分開的檔案中,註冊方法接收的引數(HtttpConfiguration 物件)和新增路由配置的方法(MapHttpRoute)也不一樣。原理都是和 [ASP.NET MVC 小牛之路]07 - URL Routing 講的一樣的,這裡不再累述了。

這個預設的Web API路由有一個靜態片段(api),還有controller和 id片段變數。和MVC路由一個關鍵不同點是,它沒有action片段變數。當然也可以和MVC一樣定義action片段變數,你可以閱讀 Routing in ASP.NET Web API 文章來了解更多Web API路由的細節。

當應用程式接收到一個和Web API 路由匹配的請求時,Action方法的呼叫將取決於傳送HTTP請求的方式。當我們用 /api/reservation URL測試API Controller時,瀏覽器指定的是GET方式的請求。API Controller的基類 ApiController根據路由資訊知道需要呼叫哪個Controller,並根據HTTP請求的方式尋找適合的Action方法。

一般約定在Action方法前加上HTTP請求方式名作為字首。這裡字首只是個約定,Web API能夠匹配到任何包含了HTTP請求方式名的Action方法。也就是說,本文示例的GET請求將會匹配 GetAllReservations 和 GetReservation,也能夠匹配 DoGetReservation 或 ThisIsTheGetAction。

對於兩個含有相同HTTP請求方式的Action方法,API Controlller會根據它們的引數和路由資訊來尋找最佳的匹配。例如請求 /api/reservation URL,GetAllReservations 方法會被匹配,因為它沒有引數;請求 /api/reservation/3 URL,GetReservation 方法會被匹配,因為該方法的引數名和URL的 /3 片段對應的片段變數名相同。我們還可以使用 POST、DELETE 和 PUT請求方式來指定ReservationController的其它Action方法。這就是前文提到的REST的風格。

但有的時候為了用HTTP方式名來給Action方法命名會顯得很不自然,比如 PutReservation,習慣上會用 UpdateReservation。不僅用PUT命名不自然,POST也是一樣的。這時候就需要使用類似於MVC的Controller中使用的Action方法選擇器了。在System.Web.Http 名稱空間下同樣包含了一系列用於指定HTTP請求方式的特性,如下所示:

public class ReservationController : ApiController {
    ...
    [HttpPost]
    public Reservation CreateReservation(Reservation item) {
        return repo.Add(item);
    }
    [HttpPut]
    public bool UpdateReservation(Reservation item) {
        return repo.Update(item);
    } 
    ...
}

從這我們也看到,在API Controller中使用東西很多都是和MVC中的Controller是一樣的,但它們分別在 System.Web.Http 和 System.Web.Mvc兩個不同的名稱空間下,正如本文開始所說,Web API把MVC中的很多東西抽取出來放在System.Web.Http名稱空間中,以使Web API作為ASP.NET平臺的一個獨立的核心。

使用 JavaScript 和 Web API 互動

在Scripts資料夾下新增一個Home資料夾,在該資料夾下新增一個 Index.js 檔案。在我們寫JS程式碼之前,先把這個JS檔案引用到Index.cshtml中,如下:

...
@section scripts {
    <script src="~/Scripts/jquery.unobtrusive-ajax.js"></script>
    <script src="~/Scripts/Home/Index.js"></script>
}
...

在 Index.js 中先把一些基本的方法寫好,如下:

function selectView(view) { 
    $('.display').not('#' + view + "Display").hide(); 
    $('#' + view + "Display").show(); 
} 
function getData() { 
    $.ajax({ 
        type: "GET", 
        url: "/api/reservation", 
        success: function (data) { 
            $('#tableBody').empty(); 
            for (var i = 0; i < data.length; i++) { 
                $('#tableBody').append('<tr><td><input id="id" name="id" type="radio"' 
                + 'value="' + data[i].ReservationId + '" /></td>' 
                + '<td>' + data[i].ClientName + '</td>' 
                + '<td>' + data[i].Location + '</td></tr>'); 
            } 
            $('input:radio')[0].checked = "checked"; 
            selectView("summary"); 
        } 
    }); 
} 
$(document).ready(function () { 
    selectView("summary"); 
    getData(); 
    $("button").click(function (e) { 
        var selectedRadio = $('input:radio:checked') 
        switch (e.target.id) {
            case "refresh":
                getData();
                break;
            case "delete":
                break;
            case "add":
                selectView("add");
                break;
            case "edit":
                selectView("edit");
                break;
            case "submitEdit":
                break;
        }
    });
});
Index.js

我們定義了三個方法。第一個 selectView 方法用於控制div的顯示和隱藏。第二個 getData 方法使用jQuery Ajax通過GET請求/api/reservation URL,並將返回的JSON資料填充到summaryDisplay的table中。第三個是jQuery的ready函式,在頁面內容載入完成時執行。這時的效果如下:

  

接下來一步一步完美編輯、儲存和刪除的功能。新增“編輯”功能程式碼如下:

...
case "edit":
    $.ajax({
        type: "GET",
        url: "/api/reservation/" + selectedRadio.attr('value'),
        success: function (data) {
            $('#editReservationId').val(data.ReservationId);
            $('#editClientName').val(data.ClientName);
            $('#editLocation').val(data.Location);
            selectView("edit");
        }
    });
    break;
...

當使用者點選編輯時,先會取得radio button的value值,並以此組成一個URL(如/api/reservation/1),HTTP請求方式(GET)和URL將使API Controller呼叫 GetReservation 方法獲取一個Reservation物件的JSON,並將其填充到editDisplay的文字框中。效果如下:

 

下面再完善一下刪除和儲存功能。程式碼如下:

...
case "delete":
    $.ajax({
        type: "DELETE",
        url: "/api/reservation/" + selectedRadio.attr('value'),
        success: function (data) {
            selectedRadio.closest('tr').remove();
        }
    });
    break;
...
case "submitEdit":
    $.ajax({
        type: "PUT",
        url: "/api/reservation/" + selectedRadio.attr('value'),
        data: $('#editForm').serialize(),
        success: function (result) {
            if (result) {
                var cells = selectedRadio.closest('tr').children();
                cells[1].innerText = $('#editClientName').val();
                cells[2].innerText = $('#editLocation').val();
                selectView("summary");
            }
        }
    });
    break;
...

根據Ajax的DELETE請求,API Controller將呼叫 DeleteReservation 將選中的Reservation物件從集合中刪除。同樣,根據PUT請求API Controller 將呼叫PutReservation方法。

最後完善一下新增功能,該ajax請求使用的是 Unobtrusive Ajax,修改 Index.cshtml 如下:

...
<h4>Add New Reservation</h4>
@{
    AjaxOptions addAjaxOpts = new AjaxOptions {
        OnSuccess = "getData",
        Url = "/api/reservation" 
    };
}
@using (Ajax.BeginForm(addAjaxOpts)) {
    @Html.Hidden("ReservationId", 0)
    <p><label>Name:</label>@Html.Editor("ClientName")</p>
    <p><label>Location:</label>@Html.Editor("Location")</p>
    <button type="submit">Submit</button>
}
...

Ajax.BeginForm生成的表單預設使用的是POST請求,相應的 API Controller將呼叫PostReservation 方法新增Reservation物件。效果如下:

 

通過這個完整的示例我們可以看到,熟悉MVC後,使用Web API也非常簡單,操作上基本和MVC類似,主要的不同體現在 ApiController 和 Action方法的匹配上。

 


參考:《Pro ASP.NET MVC 4 4th Edition》

相關文章