用ASP.NET Core 2.1 創建規範的 REST API -- HATEOAS

本文所需的一些預備知識能夠看這裏: http://www.cnblogs.com/cgzl/p/9010978.html 和 http://www.cnblogs.com/cgzl/p/9019314.htmlhtml

創建Richardson成熟度2級的POST、GET、PUT、PATCH、DELETE的RESTful API請看這裏:http://www.javashuo.com/article/p-sryarofa-ct.html 和 http://www.javashuo.com/article/p-nmpufjil-ee.html 和 http://www.javashuo.com/article/p-oxgmtvqz-dz.htmlgit

本文將把WEB API項目開始提高到Richardson成熟度3級的高度,儘管暫時尚未實現REST全部的約束,可是已經比較RESTful了。github

本文須要的代碼(右鍵另存,後綴改成zip):https://images2018.cnblogs.com/blog/986268/201806/986268-20180608085054518-398664058.jpgweb

HATEOAS(Hypermedia as the engine of application state)是 REST 架構風格中最複雜的約束,也是構建成熟 REST 服務的核心。它的重要性在於打破了客戶端和服務器之間嚴格的契約,使得客戶端能夠更加智能和自適應,而 REST 服務自己的演化和更新也變得更加容易。json

HATEOAS的優勢有:api

具備可進化性而且能自我描述數組

超媒體(Hypermedia, 例如超連接)驅動如何消費和使用API, 它告訴客戶端如何使用API, 如何與API交互, 例如: 如何刪除資源, 更新資源, 建立資源, 如何訪問下一頁資源等等. 瀏覽器

例以下面就是一個不使用HATEOAS的響應例子:服務器

{
    "id" : 1,
    "body" : "My first blog post",
    "postdate" : "2015-05-30T21:41:12.650Z"
}

若是不使用HATEOAS的話, 可能會有這些問題:架構

  • 客戶端更多的須要瞭解API內在邏輯
  • 若是API發生了一點變化(添加了額外的規則, 改變規則)都會破壞API的消費者.
  • API沒法獨立於消費它的應用進行進化.

若是使用HATEOAS:

{
    "id" : 1,
    "body" : "My first blog post",
    "postdate" : "2015-05-30T21:41:12.650Z",
    "links" : [
        {
            "rel" : "self",
            "href" : http://blog.example.com/posts/{id},
            "method" : "GET"
        },
     {
       "rel": "update-blog",
       "href": http://blog.example.com/posts/{id},
       "method" "PUT"
}
.... ] }

這個response裏面包含了若干link, 第一個link包含着獲取當前響應的連接, 第二個link則告訴客戶端如何去更新該post.

 

Roy Fielding的一句名言: "若是在部署的時候客戶端把它們的控件都嵌入到了設計中, 那麼它們就沒法得到可進化性, 控件必須能夠實時的被發現. 這就是超媒體能作到的.

針對上面的例子, 我能夠在不改變響應主體結果的狀況下添加另一個刪除的功能(link), 客戶端經過響應裏的links就會發現這個刪除功能, 可是對其餘部分都沒有影響.

HTTP協議仍是很支持HATEOAS的:

若是你仔細想一下, 這就是咱們平時瀏覽網頁的方式. 瀏覽網站的時候, 咱們並不關心網頁裏面的超連接地址是否變化了, 只要知道超連接是幹什麼就能夠.

咱們能夠點擊超連接進行跳轉, 也能夠提交表單, 這就是超媒體驅動應用程序(瀏覽器)狀態的例子.

若是服務器決定改變超連接的地址, 客戶端程序(瀏覽器)並不會由於這個改變而發生故障, 這就瀏覽器使用超媒體響應來告訴咱們下一步該怎麼作.

那麼怎麼展現這些link呢? 

JSON和XML並無如何展現link的概念. 可是HTML卻知道, anchor元素: 

<a href="uri" rel="type"  type="media type">

href包含了URI

rel則描述了link如何和資源的關係

type是可選的, 它表示了媒體的類型

爲了支持HATEOAS, 這些形式就頗有用了:

{
    ...
    "links" : [
        {
            "rel" : "self",
            "href" : http://blog.example.com/posts/{id},
            "method" : "GET"
        }
        ....
    ] 
}

method: 定義了須要使用的方法

rel: 代表了動做的類型

href: 包含了執行這個動做所包含的URI.

 

爲了讓ASP.NET Core Web API 支持HATEOAS, 得須要本身手動編寫代碼實現. 有兩種辦法:

靜態類型方案: 須要基類(包含link)和包裝類, 也就是返回的資源裏面都含有link, 經過繼承於同一個基類來實現.

動態類型方案: 須要使用例如匿名類或ExpandoObject等, 對於單個資源可使用ExpandoObject, 而對於集合類資源則使用匿名類.

 

使用靜態基類包裝類

 首先創建一個LinkResource,表示連接:

再創建一個抽象父類 LinkResourceBase:

它只有一個屬性Links。

而後我讓CityResource繼承於LinkResourceBase:

最後在Controller裏面,咱們須要寫代碼來爲資源建立上面概念提到的Links。這裏也須要用到UrlHelper,須要在Controller裏面注入。

因爲我要爲Resource建立不少基於路由的連接地址,因此須要爲相關Action的路由填上名字:

而後在Controller裏面創建一個方法,它能夠爲CityResource添加須要的Links,並返回處理後的CityResource。

首先爲資源添加的是自己的連接,這裏使用UrlHelper和路由名以及cityId做爲參數能夠獲得href,難道不須要傳遞countryId嗎?由於Controller的路由地址已經包含了countryId參數,UrlHelper會自動處理這個問題的;而rel的值能夠自行填寫,這裏我用self來表示自己,API消費者須要知道這部分,經過rel的值,API消費者就會知道API提供了哪些功能;最後method的值是GET。

其它幾個連接也是相似的。根據須要你能夠添加額外的連接,可是針對本文這個簡單的例子,這些連接就夠了。

接下來要作的就是保證每當CityResource被Action返回的時候,都會執行該方法來建立相關的連接

首先考慮返回單個City的狀況,GET:

POST也是同樣的:

還有一個GetCitiesForCountry這個方法,它返回的資源的集合,因此我須要遍歷集合,在每個資源上調用該方法:

這裏只須要使用Select方法便可,它自己就是遍歷。

測試,首先是GET單個City:

看起來是OK的,而後在用裏面的連接測試相關操做也是好用的,我就不貼圖了。

下面測試一下POST:

結果也是OK的,連接都是好用的。

最後看一下集合的GET:

看起來還不錯,集合裏的每一個資源都有正確的連接。可是結果裏並不存在針對整個集合的連接。咱們也能夠直接把結果改變成這個樣子

{
     value: [city1, city2...]
     links: [link1, link2...]    
}

由於這是不合理的JSON結果,它並非被請求的資源的類型。

 

暫時先無論這點,爲了支持集合的HATEOAS,咱們須要一個包裝類:

這個類能夠看做是針對某種類型的特殊集合,它繼承於LinkResourceBase,具備連接的屬性;此外還要保證T的類型也是LinkResourceBase,這樣就能夠保證返回的集合裏面的元素也都有Links屬性;這個類只有一個Value屬性,類型是IEnumerable<T>。

 

回到Controller再建立一個方法叫CreateLinksForCities:

 

 

注意參數和返回類型都是LinkCollectionResourceWrapper。

最後在GET Action方法裏調用該方法便可:

 

測試:

結果是能夠的,如今對於CityResource來講差很少能夠說是支持HATEOAS了。

 

使用動態類型

這裏要用到dynamic和匿名類型。

如今CountryController裏面的GET方法返回的是IEnumerable<ExpandoObject>,是塑形後的CountryResource:

我沒法把這種對象繼承於某種父類以便添加Links屬性。因此這種狀況下,就須要使用匿名類的方式。

這裏也是分單個資源和集合資源兩種狀況。

單個資源

首先爲路由添加好名稱:

因爲ExpandoObject沒法繼承我定義的父類,因此只好創建一個方法返回Links:

因爲數據塑形的存在,參數還要加上fields。前面幾個連接很好理解就是Country資源的相關連接,然後兩個資源是Country資源的子資源City的,分別是爲Country建立City和獲取Country下的Cities。

這個方法代表的咱們已是在驅動應用程序的狀態了。這也就是HATEOAS的亮點。

而後就把這些links添加到響應的body便可。首先是GET方法:

返回Links,爲ExpandoObject添加一個links屬性,並返回便可。

測試:

OK。而後咱們添加幾個數據塑形的參數:

仍然OK, self的Link裏面的href也帶着這些參數。

 

而後是POST Action的方法:

和GET差很少,只不過POST不須要數據塑形。注意返回的CreatedAtRoute裏面的第二個參數裏面的id,我是從linkedCountryResource裏面取出來的,而不是countryModel的id,這樣作也許更好,由於這個id應該是linkedCountryResource裏面的。

測試:

結果也是OK的。

集合資源

以前咱們對GetCountries作了翻頁的處理,而且把翻頁的元數據放在了響應的Header裏面,而且裏面包含了前一頁和後一頁的連接:

其實這兩個連接放在Links集合裏是更好的,因此下面這個方法會添加前一頁和後一頁的連接:

 這裏使用了以前建立的CreateCountryUri方法,分別返回了self和前一頁以及後一頁。

最後在GetCountries方法裏調用:

首先把元數據裏面的兩個連接去掉了。

而後爲集合建立了links,再而後對集合進行數據塑形,並把集合裏面的每一個對象都加上了links。最後返回一個包含value和links的匿名類。

測試:

正確的返回告終果。

下面測試一下各類參數:

結果應該是OK的,可是大小寫貌似有一些問題,這個我直接在源碼裏面改吧。

 

這裏介紹了兩種方法,其實在項目中根據狀況仍是使用一種比較好。

 

Media Type

針對響應的結果,其描述性的數據或者叫元數據應該放在Header裏面。例如以前作翻頁的時候,總頁數,當前頁數等數據都放在了Header裏面;而下一頁和上一頁的連接則放在了響應的body裏面。那這兩個連接應該是資源的一部分嗎?或者說他們是否對資源進行了描述(是不是元數據)?其它的連接也存在這個問題。若是是元數據,那麼就應該放在Header,若是是資源的一部分,就能夠放在響應的body裏。如今的狀況是,上例和以前的寫法是對同一種資源的不一樣表述。可是到目前咱們請求的Accept Header都是application/json,也就是想要資源的JSON表述,可是返回的並非Country資源的表述,而是另一種東西,它在Country資源的JSON表述的基礎上還擁有links屬性,因此說若是咱們請求的是application/json,那麼links就不該該是資源的一部分。

實際上如今返回的東西是另外一種media type而不是application/json,這樣咱們就破壞了資源的自我描述性這條約束每一個消息都應該包含足夠的信息以便讓其它東西知道如何處理該消息)。因此咱們返回的content-type的類型是錯誤的,並且還會致使API消費者沒法從content-type的類型來正確的解析響應,也就是說我沒有告訴API消費者如何來處理這個結果。那麼解決方案就是建立新的media type。

Vendor-specific media type 供應商特定媒體類型

它的結構大體以下:

application/vnd.mycompany.hateoas+json

 

第一部分vnd是vendor的縮寫,這一條是mime type的原則,表示這個媒體類型是供應商特定的。

接下來是自定義的標識,也可能還包括額外的值,這裏我是用的是公司名,隨後是hateoas表示返回的響應裏面要包含連接。

最後是一個「+json」。

整個這個media type就表示我所須要的資源表述是JSON格式的,並且還要帶着相關連接。

因此當請求的media type是application/json的時候,只須要返回資源的JSON表述。

而請求application/vnd.mycompany.hateoas+json的時候,須要返回帶有連接的資源表述。

修改Action方法:

使用FromHeader讀取Header裏面的Accept的值,而後判斷若是media type是自定義的,那麼就是包含連接的結果;不然,就使用不包含連接的結果,而且把翻頁相關的連接放在自定義的Header裏面。

測試:

請求application/json,返回結果不帶links。

修改media type:

返回的是406,Not Acceptable。

這是由於ASP.NET Core的格式化器並不認識咱們這個自定義的媒體類型。

在Startup裏面添加這兩句話以支持這個媒體類型:

而後再測試:

如今就對了。

 

根文檔

RESTful的API須要爲API的消費者提供一個根文檔。經過這個文檔,API消費者能夠知道如何與其他的API進行交互。能夠把這個理解爲索引頁面吧。

這個文檔位於API的根部,創建一個RootController:

它的路由地址就是根路徑/api。

它只有一個GET方法,經過讀取Header裏的Accept的值,來返回相應的連接。

這裏若是媒體類型是我以前自定義的那個,就會返回三個連接:自己,獲取Countries,建立Country。這三個就足夠了,有了這三個連接,其它的操做和資源(City)的路由地址都會經過一層層的連接得到到。

若是請求類型是其它的,就返回204。

因爲我這個程序太簡單了,因此這裏只寫這些內容就足夠了。

 

如今,關於資源的表述以及媒體類型你可能會發現更多的問題。

看以前的例子裏面的Links連接,這些連接的格式並非某個標準的格式,而是我本身建立的格式,消費者API並不知道如何處理這些Link,消費者API須要從API文檔中瞭解如何解析Link,我須要在API文檔裏描述rel的值。

咱們也知道媒體類型media type也是API的對外接口合約的內容。這裏還有另一個問題,超媒體容許程序控件、連接等在被須要的時候提供,針對某個動做的連接,API消費者並不知道應該在請求裏放什麼內容。

以前咱們已經建立了自定義的媒體類型,回憶一下Country的GET和POST兩個Action,它們使用的是不一樣的ResourceModel:

儘管個人例子裏它們的屬性很像,可是它們是不一樣的Model,而且有可能屬性差異很大。

而後在兩個Action裏,我都是用的是application/json這個媒體類型,實際上這個項目裏目前大部分的API我都是用的是application/json。可是實際上這兩個Model是對Country這個資源的不一樣表述,使用application/json其實是錯誤的。應該使用vendor-specific的媒體類型,例如:

application/vnd.mycompany.country.display+json和application/vnd.mycompany.country.create+json。根據狀況也能夠作的更細更靈活一些。這樣API消費者多少知道了針對不一樣動做應該發送什麼樣的請求內容了。

 

版本

咱們的API到如今已經更改了不少次,API確定會變化,因此須要版本的介入。

API的功能,業務邏輯,甚至Resource Model都會發生變化,可是咱們須要保證變化的同時不要對API的消費者形成破壞。

進行版本控制的辦法有幾個:

  • 在Uri裏面插入版本:/api/v1/countries
  • 經過query string 查詢字符串:/api/countries?api-version=v1
  • 自定義Header:例如:」api-version「=v1

可是在RESTful的世界裏,這些作法不是均可以的。

實際上Roy Fielding建議不要對RESTful API進行版本管理

可是實際上不少人感受仍是須要對API進行版本管理的,由於需求確定會一直變化的,API就會一直變化。可是也不要對任何東西都進行版本管理,咱們應該儘可能當心的使用版本,儘可能使API向下兼容

 

若是API的功能或業務邏輯變化了,HATEOAS會把這件事處理很好, API的消費者經過觀察HATEOAS的這些東西,就不會對它形成破壞。

可是若是Resource Model變化了,這確實是個問題,Roy Fielding說這種狀況也不該該進行版本管理

這些其實就是以前的問題,我如何讓API的消費者知道資源的表述應該是什麼樣的;還有我如何保證隨着API的進化,API的消費者也會跟着進化?

根據Roy Fielding的闡述,這些問題的解決方案就是使用按需編碼約束(Code on Demand)來適配媒體類型和資源表述的進化,約束中提到API能夠擴展客戶端的功能。

也許在ASP.NET MVC或者一些web網站能夠自適應這種變化,若是這些網站的js,html等是從服務器端生成的;可是大多數的時候,其實很難實現這種自適應變化。

 

咱們也許能夠在媒體類型裏添加版本號來適當處理資源表述的變化。例如:

application/vnd.mycompany.country.display.v1+json和application/vnd.mycompany.country.display.v2+json

下面舉個例子, 我在Entity Model裏面添加了一個新的屬性大洲 Continent,固然它是可空的:

而如今API的消費者能夠在建立Country的時候給Continent賦值也能夠不賦值,這時,就須要再建立一個帶有Continent屬性的ResourceModel爲POST這個動做:

別忘了作AutoMapper的映射配置。

在Controller裏,針對POST動做它的參數類型多是CountryAddResource和CountryAddWithContinentResource,因此還須要再創建一個POST的方法:

因爲有了兩個路由地址同樣的POST方法,因此還須要根據Content-Type這個Headerd的值來決定請求進入哪一個方法。這裏咱們能夠自定義一個應用於Action方法的自定義約束屬性標籤:

這個很簡單,傳進來須要匹配的header類型,和值(容許多個值);而後從request的headers裏面找到匹配便可返回true。

分別應用到兩個Action:

最後還須要把這兩個媒體類型註冊一下,注意這兩個是輸入:

 

下面測試,首先使用原來的application/json:

404,沒錯,由於Content-Type已經不符了。

接下來使用原來的POST方法的媒體類型:

就會進入原來的POST方法:

 

使用另外一個媒體類型,就會進入另一個方法,就不貼圖了是好用的。

 

上面的自定義約束標籤RequestHeaderMatchingMediaTypeAttribute的第二個參數meidatypes是個數組,爲何?

由於,就看上一個截圖,這個方法接收的格式是json,可是若是我想要也支持接收xml,就直接在數組裏添加另外一個xml的媒體類型就能夠了。

 

這個約束標籤不只僅能夠過濾一個Header類型,也能夠多個,好比說我同時還要根據Accept Header來指定不一樣的方法,那麼:

這裏提示重複,可是能夠經過修改這個約束標籤類來解決:

這時,錯誤提示就沒有了:

 

微軟的API Versioning庫

微軟提供了一個API 版本管理的庫:Microsoft.AspNetCore.Mvc.Versioning

使用Nuget安裝後,在Startup裏面註冊:

隨後就須要在Controller上標註版本了:

實際上我並非很喜歡這種版本管理,感受會很亂。。有興趣的話,請看一下官方文檔吧:

https://github.com/Microsoft/aspnet-api-versioning/wiki/New-Services-Quick-Start

隨後我把這個庫刪掉了。 

 

除了手動實現的這種HATEOAS,還有不少其它的選項,例如OData。可是OData就不只僅是HATEOAS了,它正在嘗試對RESTful API進行標準化,例如它還對建立Uri、翻頁以及調用方法等等都制定了不少規則,還有不少的東西,可是我仍是不怎麼使用OData。

 

此次就寫到這裏,源碼在:https://github.com/solenovex/ASP.NET-Core-2.0-RESTful-API-Tutorial

下週繼續。

相關文章
相關標籤/搜索