ASP.NET Web API 心得筆記 (3) CRUD 操作之後端程式

CURD 是 "Create, Read, Update, Delete" (新增、讀取、異動、刪除) 的簡寫,這四個動作是資料庫基本操作。許多的 HTTP Service 也能透過 REST 或 REST-like API 來進行 CRUD 操作。

以下會進行一個非常簡單的連絡人管理。透過以下方法來提供服務。

Web API CRUD 方法說明
Action說明HTTP方法URI關連
取得所有連絡人清單GET/api/contacts
透過 id 取得連絡人資料GET/api/contacts/id
新增一位連絡人POST/api/contacts
更新連絡人資料PUT/api/contacts/id
刪除連絡人資料DELETE/api/contacts/id

由上表中,我們可以很清楚看到,有兩種資源類型( resource types):

URI 資源類型
URI說明
/api/contacts列出所有連絡人
/api/contacts/id一位連絡人

HTTP 方法


HTTP 主要方法 (GET, PUT, POST, DELETE) 能對應到 CURD 操作:
  • GET 接收及顯示。GET 在伺服器應該沒有副作用。
  • PUT 更新。PUT 也能拿來"新增"使用,如果伺服器允許 Client 去指定新的 URI。那範例的連絡人管理將不允許使用 PUT 來新增。
  • POST 新增。伺服器會給 URI 分配新物件,然後返回此 URI 作為回應訊息的一部分。
  • DELETE 刪除

ps. 現在開啟前一篇的 HelloWebAPI 專案。相關步驟不再重覆。


新增資源


在 ASP.NET Web API,你能在 Model 使用強型別 CLR 物件,他們將會自動序列化為 XML 或 JSON 給 Client。

Model 目錄新增 Contact 類別

''' <summary>
''' 連絡人資訊欄位
''' </summary>
Public Class Contact
    Property Id As Integer
    Property Name As String
    Property Phone As String
    Property Email As String
End Class

使用倉儲模式( Repository Pattern)


我們的 HTTP Service 需要儲存連絡人清單,此範例,連絡人清單會儲存在記憶體( List(Of T ) )。使用 Repository Pattern 會讓此物件從我們的 Service 實作中切割出來。

Model 目錄,新增一個 Interface 檔案,名稱 IContactRepository.vb

''' <summary>
''' 連絡人 Repository Pattern 介面定義
''' </summary>
Public Interface IContactRepository
    ''' <summary>
    ''' 取得所有連絡人
    ''' </summary>
    ''' <returns>IQueryable(Of Contact)</returns>
    Function GetAllContact() As IQueryable(Of Contact)

    ''' <summary>
    ''' 透過 id 取得連絡人
    ''' </summary>
    ''' <param name="id">編號</param>
    ''' <returns>連絡人物件</returns>
    Function GetContactById(id As Integer) As Contact

    ''' <summary>
    ''' 新增連絡人
    ''' </summary>
    ''' <param name="item">連絡人物件</param>
    ''' <returns>連絡人物件</returns>
    Function AddContact(item As Contact) As Contact

    ''' <summary>
    ''' 刪除連絡人
    ''' </summary>
    ''' <param name="id">編號</param>
    Sub RemoveContact(id As Integer)

    ''' <summary>
    ''' 更新連絡人
    ''' </summary>
    ''' <param name="item">連絡人物件</param>
    ''' <returns>True 成功更新</returns>
    Function UpdateContact(item As Contact) As Boolean
End Interface

以上定義出我們需要的 CRUD 相關功能介面,然後一樣在 Model 目錄 下新增一個新的類別檔,類別檔名稱 "ContactRepository.vb",此類別將實作 IContactRepository 介面。以下為相關實作:

''' <summary>
''' 實作 IContactRepository 介面
''' </summary>
Public Class ContactRepository
    Implements IContactRepository

    ' 存取連絡人清單
    Private _contacts As New List(Of Contact)()
    ' id 指標
    Private _nextId As Integer = -1

    ''' <summary>
    ''' 建構式
    ''' </summary>
    ''' <remarks>新增連絡人資料</remarks>
    Sub New()
        Me.AddContact(New Contact() With {.Name = "艾利思Alice",
                                          .Email = "alice@kkbruce.net"})
        Me.AddContact(New Contact() With {.Name = "布魯斯Bruce",
                                          .Email = "bruce@kkbruce.net"})
        Me.AddContact(New Contact() With {.Name = "查理斯Charles",
                                          .Email = "charles@kkbruce.net"})
    End Sub

    ''' <summary>
    ''' 新增連絡人
    ''' </summary>
    ''' <param name="item">連絡人物件</param>
    ''' <returns>連絡人物件</returns>
    Public Function AddContact(item As Contact) As Contact _
                    Implements IContactRepository.AddContact

        ' 自動增加 id 編號
        item.Id = _nextId + 1
        ' 將物件加入至 List 的結尾
        _contacts.Add(item)
        Return item
    End Function

    ''' <summary>
    ''' 取得所有連絡人清單
    ''' </summary>
    ''' <returns>IQueryable(Of Contact)</returns>
    Public Function GetAllContact() As IQueryable(Of Contact) _
                    Implements IContactRepository.GetAllContact

        ' 將 IEnumerable 轉換成 IQueryable 回傳
        Return _contacts.AsQueryable()
    End Function

    ''' <summary>
    ''' 透過 id 找尋特定連絡人
    ''' </summary>
    ''' <param name="id">編號</param>
    ''' <returns>連絡人物件</returns>
    Public Function GetContactById(id As Integer) As Contact _
                    Implements IContactRepository.GetContactById

        ' 並傳回整個 List 內第一個相符的元素
        Return _contacts.Find(Function(c) c.Id = id)
    End Function

    ''' <summary>
    ''' 移除連絡人
    ''' </summary>
    ''' <param name="id">編號</param>
    Public Sub RemoveContact(id As Integer) _
                    Implements IContactRepository.RemoveContact

        ' 並傳回整個 List 內第一個相符的元素
        Dim contact = _contacts.Find(Function(c) c.Id = id)
        ' 從 List 移除特定物件的第一個相符項目
        _contacts.Remove(contact)
    End Sub

    ''' <summary>
    ''' 更新連絡人
    ''' </summary>
    ''' <param name="item">連絡人物件</param>
    ''' <returns>Boolean, True 更新成功</returns>
    Public Function UpdateContact(item As Contact) As Boolean _
                    Implements IContactRepository.UpdateContact

        ' 並傳回 List 內或它的一部分中第一個相符元素之以零起始的索引
        Dim index As Integer = _contacts.FindIndex(Function(c) c.Id = item.Id)
        If index = -1 Then
            Return False
        End If

        ' 移除 List 中指定之索引處的項目
        _contacts.RemoveAt(index)
        ' 將物件加入至 List 的結尾
        _contacts.Add(item)
        Return True
    End Function
End Class

實作沒有什麼難處,要處理都是 List(Of Contact) 所提供的 .Add(), .Remove(), .Find() 以進行相關新增、刪除、搜尋動作。另外,還利用 LINQ 的 .AsQueryable() 來將 List 型別轉換,才有辦法以 IQueryable() 來回傳。以上利用 List 來模擬資料庫,或者說,把 List 想像成記憶體裡的資料庫。

新增 Web API Controller


Controllers → New → Controller...

新增 Controller
圖一:新增 Controller
選擇 Controller Template
圖二:選擇 Controller Template

  1. Controller name: ContactsController
  2. Template: Empty API controller


接下來,我們就可以撰寫此 HTTP Service 的程式碼,注意命名開始要對應到 HTTP Method。我們先寫下我們的引用等一下會用到的命名空間及第一行程式碼。

Imports System.Web.Http
Imports System.Net.Http
Imports System.Net

Public Class ContactsController
    Inherits ApiController

    ' Respository Pattern
    Shared ReadOnly _repository As IContactRepository = New ContactRepository()

End Class

取得資源

取得資源是 Read 與 GET 的對應關係。在連絡人管理中提供了二個 Action,一個是讀取所有連絡人,一個是透過 id 來取得連絡人。這兩個 Action 都定義在 HTTP GET 方法,記得方法必須以 "Get..." 開頭。
  • GET /api/contacts
  • GET /api/contacts/id

' Get /api/contacts
''' <summary>
''' 取得所有連絡人資料
''' </summary>
Function GetAllContacts() As IEnumerable(Of Contact)
    Return _repository.GetAllContact()
End Function

GetAllContacts() 方法會對應至 GET /api/contacts。

' Get /api/contacts/id
''' <summary>
''' 透過 id 取得連絡人資料
''' </summary>
''' <param name="id">編號</param>
Function GetContact(id As Integer) As Contact
    Dim contact As Contact = _repository.GetContactById(id)
    If contact Is Nothing Then
        Throw New HttpResponseException(System.Net.HttpStatusCode.NotFound)
    End If
    Return contact
End Function

GetContact(id As Integer) 會對應至 GET /api/contacts/id。我們傳入的 id 是字串,但 ASP.NET Web API 會自動轉換傳入的 id 到正確的資料型別 ( Integer )。

我們在 GetContact() 裡還做了錯誤處理,此例外為 System.Web.Http.HttpResponseException 類別,此命名空間必須在 .NET Framework 4.5 Beta 中才會提供。原文解釋:The exception that is thrown to allow for a given HttpResponseMessage to be returned to the client.

新增資源

新增資源是 Create 與 POST 的對應關係。要新增一位連絡人,Client 送出一個 HTTP POST 請求,請求訊息包含新連絡人的相關內容。記得方法必須以 "Post..." 開頭。

這裡我們很直覺的寫以下程式碼:

' Add
''' <summary>
''' 新增連絡人 ( 非最後完成實作 )
''' </summary>
''' <param name="contact">連絡人物件</param>
Function PostContact(contact As Contact) As Contact
    contact = _repository.AddContact(contact)
    Return contact
End Function

預設下,從請求主體(request body)來而的參數解序列化後是複合(complex)型別。因此,我們預期 Client 傳送給我們的是一個經序列化表現的連絡人物件,使用 XML 或 JSON 來序列化。

以上的實作可以運作,但未考慮到兩件事:

  • Response code
    預設,Web API Framework 設定回傳狀態碼(status code)為 200 (OK)。但按照 HTTP/1.1 協定,當一個 POST 請求會導致資源的建立,伺服器應該回應的狀態碼為 201 (Created)。
  • Location當伺服器新增一個資源,它應該在回應的 Location header 中包含新資源的 URI。

ASP.NET Web API 讓你可以很簡易操作 HTTP 回應訊息,我們看改善後的 POST 實作。

' Add
''' <summary>
''' 新增連絡人
''' </summary>
''' <param name="contact">連絡人物件</param>
''' <returns>System.Net.Http.HttpResponseMessage for .NET Frameowrk 4.5</returns>
Function PostContact(contact As Contact) As HttpResponseMessage(Of Contact)
    contact = _repository.AddContact(contact)

    ' .NET Frameowrk 4.5 才有提供
    ' http://msdn.microsoft.com/en-us/library/system.net.http.httpresponsemessage(v=vs.110).aspx
    ' Represents a HTTP response message.
    Dim response = New HttpResponseMessage(Of Contact)(contact, HttpStatusCode.Created)

    ' 設定路徑的 URL 模式。
    Dim uri As String = Url.Route(Nothing, New With {.id = contact.Id})
    ' Headers : Gets the collection of HTTP response headers.
    response.Headers.Location = New Uri(Request.RequestUri, uri)
    Return response
End Function

以上就是做 Response code 與 Location 兩件事。

注意回傳型別是 HttpResponseMessage(Of Contact)HttpResponseMessage(Of T) 型別是一個用強型別表示的 HTTP 回應訊息。泛型參數 T 會取得 CLR 型別然後序列化到訊息主體。

在建構式,我們指定連絡人的執行個體 (contact) 來序列表且傳入 HTTP 狀態碼來回傳:

Dim response = New HttpResponseMessage(Of Contact)(contact, HttpStatusCode.Created)

HttpResponseMessage(Of T) 類別也讓你操作回應標頭(headers),在此範例中,我們設定 Location 標頭在一個新連絡人的 URI。

Dim uri As String = Url.Route(Nothing, New With {.id = contact.Id})
response.Headers.Location = New Uri(Request.RequestUri, uri)

更新資源

更新資源是 Update 與 PUT 的對應關係。更新連絡人作法相當直覺,記得方法必須以 "Put..." 開頭。

' Updating
''' <summary>
''' 更新連絡人
''' </summary>
''' <param name="id">編號</param>
''' <param name="contact">連絡人物件</param>
Sub PutContact(id As Integer, contact As Contact)
    contact.Id = id
    If Not _repository.UpdateContact(contact) Then
        Throw New HttpResponseException(HttpStatusCode.NotFound)
    End If
End Sub

UpdateContact() 會回傳 True 或 False,我可以進行判斷是否更新成功。方法中有二個參教,連絡人 id 與 更新的連絡人資料。id 參數會從 URI 路徑取得,連絡人參數是從請求主體解序列化而來。預設,ASP.NET Web API Framework 會從請求主體 (request body)裡的 route 與 複合(complex) 型別來取得簡易的參數型別。

刪除資源

刪除資源是 Delete 與 DELETE 的對應關係。

' Deleting
''' <summary>
''' 刪除連絡人
''' </summary>
''' <param name="id">編號</param>
''' <returns>HttpResponseMessage</returns>
Function DeleteContact(id As Integer) As HttpResponseMessage
    _repository.RemoveContact(id)
    Return New HttpResponseMessage(HttpStatusCode.NoContent)
End Function

依居 HTTP 規範,DELETE 方法必須是 idmpotent(冪等),意味著幾個相同 URI 的刪除請求必須和一個刪除請求有相同效果。因此,如果連絡人已經被刪除,方法應該不能回傳錯誤碼。

如果 DELETE 請求成功,你能回傳狀態 200 (OK) 描述該實體主體(即要刪除的主體)的狀態,或如果刪除持續未處理回傳狀態 202 (Accepted) ,或沒有實體主體回傳狀態 204 (No Content)。我們範例會回傳狀態 204 (No Content)。

CRUD 操作總結

在使用 ASP.NET Web API Framework 時,你能發現與 HTTP/1.1 規範有很大關連性,以前較很少關心與了解的內容,例如,PUT、DELETE、POST的處理,以及狀態碼的處理等,現在變成要開注意的幾個點。


參考資料

1 則留言:

  1. 高手您好
    Function PostContact(contact As Contact) As HttpResponseMessage(Of Contact)
    Dim response = New HttpResponseMessage(Of Contact)(contact, HttpStatusCode.Created)
    這兩個地方在錯誤清單上會出現
    錯誤 1 'System.Net.Http.HttpResponseMessage' 沒有型別參數,因此不可以有型別引數。 C:\Users\it\Documents\Visual Studio 2012\Projects\HelloWebAPI\HelloWebAPI\Controllers\ContactsController.vb 39 72 HelloWebAPI

    請指導是哪裡出問題嗎?



    回覆刪除

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