2012年5月18日

ASP.NET MVC Web API - 利用jQuery進行CRUD! (二) Controller篇

支援版本

  • ASP.NET MVC 4 Beta

上一篇寫完後,沒想到喝個淡定紅茶,就淡定到現在;前篇介紹了Model的實作,現在我們準備要進入Controller了喔,也就是開始撰寫Web Service!

REST

在開始寫Controller之前,我們要稍微回憶一下REST風格,所謂的REST風格,就是同一個URI下,利用HTTP的四大命令"Get、Post、Put、Delete"來配合CRUD,而不像以前取得資料是一個網址,刪除資料又是一個網址,我們可以看看下面的表。

URI Get Post Put Delete
http://xxx/api/customer 取得整組資料 新增整組資料 更新整組資料 刪除整組資料
http://xxx/api/customer/12 取得單筆資料 新增單筆資料 更新單筆資料 刪除單筆資料

從這邊我們可以看到,URI ( 資源,其實就是一個網址 )都是同一個,但是我們針對不同的HTTP命令,就會有不同的效果,而這些效果剛好符合CRUD,而這邊在強調一下,REST並不是一種規範,而是一種風格,利用HTTP的四大命令來對應資料庫的CRUD,但實際是你也可以使用Get命令來執行DB的Delete,但這樣就不符合REST風格了。

建立Web API吧

了解了這些規則後,接下來,我們要做的就是,如何使用ASP.NET MVC 建立符合REST風格的Web API;首先,第一步當然是先建立Controller嚕,也就是這篇的主題。

image

接下來,先把CustomerController名稱打好,但是這次的樣板則是選擇API controller with empty read/write actions這種樣板了喔!

image

完成後,我們會發現,系統自動幫我們準備好了這些程式碼,建立好的Class裡面的Function,也剛好就是上頭提到的Get、Post、Put、和Delete!!剛好就對應到HTTP的四個命令!

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

namespace MvcWebApiCRUDDemo.Controllers
{
    public class CustomerController : ApiController
    {
        // GET /api/customerontroller
        public IEnumerable Get()
        {
            return new string[] { "value1", "value2" };
        }

        // GET /api/customerontroller/5
        public string Get(int id)
        {
            return "value";
        }

        // POST /api/customerontroller
        public void Post(string value)
        {
        }

        // PUT /api/customerontroller/5
        public void Put(int id, string value)
        {
        }

        // DELETE /api/customerontroller/5
        public void Delete(int id)
        {
        }
    }
}

修改開始

不過這畢竟不是我們要的,如果能自動產生出我們想要的東西,就好了,但現實總是殘酷的;所以我們要重新修改一下程式碼,完成後的程式碼會長這樣,大家可以先看一下,細節我們下面會再仔細介紹。

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

namespace MvcWebApiCRUDDemo.Controllers
{
    public class CustomerController : ApiController
    {
        //建立一個靜態的倉儲,這裡使用靜態的目的是為了讓資料可以被CRUD,
        //而不會因,重新建立此倉儲物件,而造成重塞資料的問題。
        private static readonly ICustomerRepository _repository = new CustomerRepository();

        // GET /api/customer
        public IEnumerable Get()
        {
            return _repository.GetAll();
        }

        // GET /api/customer/5
        public Customer Get(int id)
        {
            Customer customer = _repository.Get(id);
            if (customer == null)
            {
                //如果找不到,就拋出HTTP的Response例外,內容是尋找不到,也就是404
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
            return customer;
        }

        // POST /api/customer
        public HttpResponseMessage Post(Customer customer)
        {
            customer = _repository.Add(customer);
            var response = new HttpResponseMessage(customer, HttpStatusCode.Created);

            string uri = Url.Route(null, new { id = customer.Id });
            response.Headers.Location = new Uri(Request.RequestUri, uri);

            return response;
        }

        // PUT /api/customer/5
        public void Put(int id, Customer customer)
        {
            customer.Id = id;
            if (!_repository.Update(customer))
            {
                //如果找不到,就拋出HTTP的Response例外,內容是尋找不到,也就是404
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
        }

        // DELETE /api/customer/5
        public HttpResponseMessage Delete(int id)
        {
            _repository.Delete(id);
            return new HttpResponseMessage(HttpStatusCode.NoContent);
        }
    }
}

Repository

首先我們在這個Controller先建立一個static的Repository類別,這個類別就是我們上一章準備好的倉儲物件,也就是會提供給我們新增修改Collection方法的地方 ( 別忘了,我們這個範例使用Collection來代替資料庫 ),這邊使用靜態物件的原因,是因為,如果使用非靜態物件,每次產生這個倉儲物件時,都會自動塞資料到Collection,所以我們利用靜態物件來處理,這樣子,這個倉儲物件,就只會產生一次,淡定的在那邊=v=。

( 備註一下,並不是Repository Patten都要使用靜態物件!這裡使用靜態的原因就如上面所說,但如果今天我們是針對資料庫,我們就可以不需要使用靜態物件,另外,有人可能會疑惑,為什麼要使用介面,這其實是為了測試阿!!,當我們希望針對這個Controller進行Unit Test的時候,我們就可以Mock一個假的倉儲物件,來"模擬"新增修改刪除,所以這邊才要使用介面,這也就是使用Repository Patten的精隨。 )

private static readonly ICustomerRepository _repository = new CustomerRepository();

當有了倉儲物件,我們就可以輕易地去做新增修改等方法,如下,這就是很典型的取得所有資料的方法。

_repository.GetAll();

就是這麼簡單。

Get

接下來,我們看看第一個Get方法,這個方法其實就會對應到HTTP的Get命令,簡單的說,當我們對/api/customer這個URI,使用HTTP的Get命令,就會執行到這個Function,然後就會取得一堆JSON的資料,這就是ASP.NET MVC Web API的特色;當然,如果熟悉ASP.NET MVC的人,可能會想說,Get這個Action ( 對MVC來說Action其實就是個Function )的網址應該是xxx/Get阿!?以前不都是這樣搞的嗎!?,但不要懷疑,的確也可以像以前一樣的網址,但這部分後續章節才會講到。回頭看Web API,Web API 裡面的Function名稱,如果取名為Get,就會對應到HTTP的Get命令,反正只要記住,在ASP.NET MVC Web API底下( 也就是繼承了ApiController的Controller ),預設的Function名稱,只要對應到HTTP四大命令的名稱,就是會去對應到HTTP的四大命令。另外,如果有人看過ASP.NET MVC官網的範例,會發現,人家的範例是寫GetAllContacts(),不用懷疑,其實Function的名稱,只要前面有符合HTTP四大命令的名稱,就會對應到了。

// GET /api/customer
public IEnumerable Get()
{
    return _repository.GetAll();
}

如果使用IE進行分析,就可以看到對這個網址進行了GET請求。( 底下的圖,是整個專案寫完後,執行並截圖下來的,因為View的地方還沒有講到,所以這邊就只截分析的片段,等講道View的部分,會再帶大家看一次。 )

image

會回應JSON格式,沒錯,不用懷疑,ASP.NET MVC 預設就是回應JSON的格式,基本上JSON的格式還滿簡單的,如果對JSON不了解的人,可以參考一下這篇的中間,或是這篇的最後面有參考連結。

image

還是Get

接下來,我們繼續往下看,接下來,還是Get,但這次的Get帶了參數,沒錯,這個Get的用途就是取得單筆的資訊,我們可以透過/api/customer/5,來取得id為5的資料回來;而如果找不到,我們就會拋出一個例外,這個例外大家應該很熟習,就是傳說中的404。

// GET /api/customer/5
public Customer Get(int id)
{
    Customer customer = _repository.Get(id);
    if (customer == null)
    {
        //如果找不到,就拋出HTTP的Response例外,內容是尋找不到,也就是404
        throw new HttpResponseException(HttpStatusCode.NotFound);
    }
    return customer;
}

我們會發現,還是使用Get( 廢話XDD )。

image

這裡只回應單筆了。

image

Post

然後接著的是Post,也就是準備要做新增的處理,但這裡的新增有比較不一樣的地方,首先,我們會將Customer這個Model傳進去來處理,然後當然也是用倉儲Class來進行新增的動作,但比較特別的返回型別,這裡返回的是一個HttpReponseMessage,那是因為,Web API框架預設的回應狀態為200(OK),但在HTTP/1.1的協定裏面,如果使用POST創立一個資源時,因該是要返回201( Created ),這部分未來不知道正式版會不會修正,但現階段,我們只能自己手寫的方式( 或是直接拷貝貼上的方式XDD ),來處理201的狀態,這也就是為什麼需要返回一個HttpResponseMessage型別的原因;那這樣就ok了嗎?當然不,根據協定,除了要返回201以外,還必須在回應的Headers裡面的Location加上新資源的URI,這樣才是完整的!!反正結論就是必須加上下面這些東西,還是衷心的期望,正式版可以符合懶人小弟我的需求XDDD

// POST /api/customer
public HttpResponseMessage Post(Customer customer)
{
    customer = _repository.Add(customer);
    var response = new HttpResponseMessage(customer, HttpStatusCode.Created);

    string uri = Url.Route(null, new { id = customer.Id });
    response.Headers.Location = new Uri(Request.RequestUri, uri);

    return response;
}

依照慣例,我們還是要貼一下圖,這次我們使用了POST了。

image

然後我們可以看一下,送出去了那些東西 ( 好啦,我承認我懶惰,把名字和電話都打"2" )

image

接下來,我們可以看到,回應的部分,的確如預期是回應201 Created了,並且在Location裡面有附上URI,因為是第四筆,所以後面多了4。

image

接下來,我們來看看Update。

Update

Update對應的其實就是HTTP的Put,這大家應該就比較少見了,但是其實還是依樣簡單,基本上帶入兩個參數,第一個參數id,是透過網址的方式傳進來的,第二個參數Customer,未來則是會用JSON格式傳遞過來,那內容其實也很簡單,畢竟我們將更新的方法都封裝到Repository Class裡面去了,我們只要簡單的呼叫_repository.Update就可以了,如果回傳false,就跳出錯誤訊息,代表找不到。

// PUT /api/customer/5
public void Put(int id, Customer customer)
{
    customer.Id = id;
    if (!_repository.Update(customer))
    {
        //如果找不到,就拋出HTTP的Response例外,內容是尋找不到,也就是404
        throw new HttpResponseException(HttpStatusCode.NotFound);
    }
}

這次使用PUT了,所以會看到,請求使用PUT命令,而且可以看到網址最後有1,表示我們更改第一筆。

image

這次稍微多打了幾個字,我們把第一筆資料的name改為MS,phone也改為MS。( 由此可知,我們的UI和後面的Server端是完全沒檢查的XDDD,未來在教大家如何使用Model進行驗證,方法和ASP.NET MVC的方法其實是一樣的喔! )

image

Delete

終於寫到最後一個了,寫到快斷氣了= =,Delete通常都是號稱最簡單的XDD,( 破壞果然比建設容易 ),但這邊比較特別的是,這裡會返回一個204,也就是NoContent,根據HTTP/1.1,如果刪除了,可以返回200,或是202 ( 表示已經接受,但還沒刪除 ),或是204 ( 代表的是完成了請求,且不需要返回任何東西 ),這裡204是最適用於Delete,詳細可以參考這裡

// DELETE /api/customer/5
public HttpResponseMessage Delete(int id)
{
    _repository.Delete(id);
    return new HttpResponseMessage(HttpStatusCode.NoContent);
}

然後,我們依樣看一下圖,這次使用了DELETE命令。

image

回應的是204 No Contant。

image

後記

這篇又打了超久,但至少未來如果自己懶得打程式,也可以直接拷貝和貼上使用XDD;我們從這邊可以感受到Repository Patten所帶來的一些好處,也深深的了解到ASP.NET MVC Web API的寫法,更加知道了Web Service應該回應的status,下一篇,我們將進入View的世界,使用JavaScript和jQuery來撰寫AJAX的應用程式,來呼叫這裡寫好的Web Service!!

( 先去準備打Diablo 3了.. )

參考資料

10 則留言:

  1. 您好,拜讀您的文件後,不完全的照本宣科做了部分的學習,我想請問,我在Controller中的Get 或是Get(1)透過IE去收集資訊後,返回的都是型別,並非Json字串,是不是有甚麼地方還需要調整呢?

    回覆刪除
  2. 匿名的大大您好,很抱歉,這幾天都一直在狂忙,直到最近才看到您的問題;因為這樣的資訊有點少,小弟我可能會比較難得知是哪裡有誤。
    但如果是做了部分的練習,可以看一下您的Controller,是否是繼承ApiController,正確的繼承應該如下:
    public class CustomerController : ApiController
    希望能對您有點幫助,謝謝。

    回覆刪除
  3. 你好!感謝分享好文章!

    但我在複製你的程式碼後編譯出錯,錯誤訊息如下:
    非泛型 型別 'System.Net.Http.HttpResponseMessage' 不能配合型別引數使用

    發生錯誤的那行程式碼是:
    var response = new HttpResponseMessage(customer, HttpStatusCode.Created);

    奇怪的是,我是用VS2012(.NET 4.5)、System.Net.Http.dll有加入參考、也有using了

    不知為何編譯器找不到HttpResponseMessage這個類別

    System.Net.Http.dll的版本為4.0.0.0

    請大大告訴我,謝謝!

    回覆刪除
    回覆
    1. 不知為何編譯器找不到HttpResponseMessage<T>這個類別

      刪除
  4. 作者已經移除這則留言。

    回覆刪除
  5. Hello!!不好意思現在才看到這個留言,關於Searchers大大您的問題,其實很簡單,因為正式版本後,又改了 Orz.... ( 嗚嗚嗚,現在都改很快....QQ )

    以post那邊的寫法,你可以改成
    HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.Created, customer);
    response.Headers.Location = new Uri(Url.Link(null, new { id = customer.Id }));
    這樣應該就可以了。( 不過我還沒實際測試過>"<....最近比較忙.... )

    如果你有安裝新的ASP.NET and Web Tools 2012.2 ( http://www.asp.net/vnext )
    那裏面有一個"單一頁面應用程式"的樣板,裡面也有完整的CRUD範例;也可以參考看看。

    最後,還是很感謝您提出的問題,謝謝~~

    回覆刪除
  6. 你好! 我目前正在學習用Web API來開發RESTful Web Service
    參照您的教學後發現了一點問題:

    在POST方法裡面的 customer = _repository.Add(customer);

    在Add的地方會出" ICustomerRepository 不包含Add的定義"的錯誤

    思考後也試了一陣子還是解不掉這問題...想在這邊請教您一下, 謝謝!

    回覆刪除
  7. HI HI!!
    CustomerRepository是實作了ICustomerRepository 這個interface,
    你可以檢查看看這個interface有沒有定義了Add ( 話說,剛剛看了一下文章,好像沒把interface給貼出來阿.... )QQ
    ok...如果朋友您的interface沒事先定義,其實也是可以做下去的,你不要讓CustomerRepository實作interface,也是可以的...我們這邊使用interface其實是為了以後的DI,如果不熟,也可以直接使用CustomerRepository即可喔!!~

    回覆刪除
    回覆
    1. HI,
      看到您回覆的不久前我發現是我自己的問題XDD 謝謝!
      另外想請問上面提到的DI是指什麼呢?!

      刪除
  8. hi~
    DI是Dependency Injection,你可以去Google查查看,滿多前輩寫了很多不錯的相關文章喔!~

    回覆刪除