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

来源:https://www.cnblogs.com/cgzl/archive/2018/06/09/9153749.html
-Advertisement-
Play Games

本文所需的一些預備知識可以看這裡: http://www.cnblogs.com/cgzl/p/9010978.html 和 http://www.cnblogs.com/cgzl/p/9019314.html 建立Richardson成熟度2級的POST、GET、PUT、PATCH、DELETE的 ...


本文所需的一些預備知識可以看這裡: http://www.cnblogs.com/cgzl/p/9010978.html 和 http://www.cnblogs.com/cgzl/p/9019314.html

建立Richardson成熟度2級的POST、GET、PUT、PATCH、DELETE的RESTful API請看這裡:https://www.cnblogs.com/cgzl/p/9047626.html 和 https://www.cnblogs.com/cgzl/p/9080960.html 和 https://www.cnblogs.com/cgzl/p/9117448.html

本文將把WEB API項目開始提升到Richardson成熟度3級的高度,儘管暫時還沒有實現REST所有的約束,但是已經比較RESTful了。

本文需要的代碼(右鍵另存,尾碼改為zip):https://images2018.cnblogs.com/blog/986268/201806/986268-20180608085054518-398664058.jpg

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

HATEOAS的優點有:

具有可進化性並且能自我描述

超媒體(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

下周繼續。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Time Limit: 9000/3000 MS (Java/Others) Memory Limit: 131072/65536 K (Java/Others)Total Submission(s): 16213 Accepted Submission(s): 4992 Problem Descr ...
  • 不同平臺框架項目使用同一套代碼,一次編譯生成多個框架類庫 [TOC] 需要先瞭解的東西 "msbuild" .net framework遷移至.net core,或者合併,單個項目編譯不同的框架 —— "官方文檔" 分析 使用 ".NET 可移植性分析器工具" 分析項目依賴 .NET 可移植性分析器 ...
  • A practical tutorial showing how to use JSON Web Tokens in ASP.NET Core 2 applications. Heads up! This article refers to version 2.0 of the ASP.NET Co... ...
  • HTTPS為SSL安全通道,雖然並不清楚具體有什麼用,但至少網站看上去比HTTP上檔次,訪問速度也沒什麼影響,所以有條件的話,還是做下,可以做噱頭忽悠人。 WIN2008系統 因為埠443衝突,只能部署一個站點,切記;WIN2012不限制。 SSL證書有免費的,有收費的,五花八門,免費的有些不受G ...
  • using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ConsoleApp1 //List 的聲明與使... ...
  • ...
  • using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApp1 //函數的遞歸調用 { //F(n)= F(n-1)+F(n-2)... F(1)=... ...
  • 概述 博主自畢業後,進公司就一直是以ASP.NET MVC 5.0 + MySQL 進行項目開發,在項目也使用了很多常用功能,如 WCF、SignalR、微信公眾號API、支付寶API、Dapper等等,前端是大雜燴,如:Bootstrap、AmazeUI、EasyUI、Light7、WeUI等等。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...