用ASP.NET Core 2.0 創建規範的 REST API -- DELETE, UPDATE, PATCH 和 Log

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

創建Richardson成熟度2級的POST和 GET的RESTful API請看這裏:http://www.javashuo.com/article/p-sryarofa-ct.htmlgit

以前一篇文章介紹了POST和GET,這篇要介紹創建Richardson成熟度2級的DELETE, PUT, PATCH.github

本文須要用到的代碼(右鍵另存,後綴改成zip): https://images2018.cnblogs.com/blog/986268/201805/986268-20180524161857994-217513181.jpg數據庫

DELETE 刪除資源

這個很簡單,以刪除City爲例:json

首先查找Country,沒找到就返回404 Not Found;而後查找City,沒找到也返回 404 Not Found;若是找到了,刪除保存的時候失敗,則返回 500 Internal Server Error;若是刪除成功,則不須要返回什麼內容,返回204 No Content便可。windows

測試:設計模式

若是再次執行該請求的話,不出意外的會返回 404 Not Found:api

DELETE並不具備安全性,由於在方法執行後會改變資源(把資源刪除了)。數組

可是DELETE是具備冪等性的,這個你可能會有疑問,我執行屢次DELETE後返回的狀態碼不同爲何還具備冪等性。安全

以前我提過冪等性的簡單定義,那個定義多少有點模糊,咱們再來看一下冪等性定義裏關鍵的一句話:「the side-effects of N > 0 identical requests is the same as for a single request」,意思是屢次請求的反作用和單次請求的反作用是同樣的。冪等性的核心概念能夠理解爲:"你能夠發送多於一次的一樣請求,可是不會對服務器形成額外的改變"。也就是說每次發送了DELETE請求以後,服務器的狀態都是同樣的

 

一塊兒刪除主從資源

這種狀況也很常見,在刪除Country資源的同時,把它的子資源City也刪掉。

這個很簡單,因爲EFCore作了不少工做,就不須要在刪除主資源的時候手動去刪除它全部的子資源了。

測試:

 

刪除集合資源

DELETE "http://localhost:5000/api/countries",這個請求是合理的。可是確實不多這麼作,由於這麼作的破壞性仍是挺大的。。。

 

PUT 更新資源

Put應該用來對資源的總體更新

因爲PUT是對資源的總體修改,請求body中應該帶着更新對象,因此先創建這個對象:

自己City這個Model就只有兩個字段,而id的應該做爲路由的參數傳遞進來,因此在CityUpdateResource裏面就不須要id屬性了;若是有Id的話,你可能還要與路由參數裏的id進行比較,若是不一樣會帶來麻煩,因此這個對象裏不帶id。

這時你也能夠發現CityUpdateResource和CityAddResource所含有的屬性是同樣的,那麼爲何不使用同一個類型呢?由於這兩個對象的目的不一樣,責任不一樣,一個類只應該有一個責任(SRP)。可是你可使用某個父類把相同的屬性抽取出去,而後分別繼承,可是我就不這樣作了。

下面看這個PUT的Action方法:

這個方法也很簡單,其中有兩點須要注意:怎麼把傳遞進來的對象的全部屬性值都傳遞給EFCore的Model?這裏使用AutoMapper便可,上面紅框的方法就是把第一個參數對象的屬性映射到第二個參數對象上。

再有就是應該返回什麼?我認爲Ok和NoContent都是能夠的,若是在Action的方法裏某些屬性的值是在這裏改變的,那麼可使用Ok把最新的對象傳遞回去;可是若是在Action方法裏沒有再修改其它屬性的值,也就是說更新以後和傳遞進來的對象的屬性值是同樣的,那就沒有必要再把最新的對象傳遞回去了,這時就應該使用NoContent。

再看一下Repository裏面:

注意這個是DbContext的方法而不是DbSet的方法,它會追蹤city,而後把它的ModelState設置爲Modified。

測試:

OK.

下面作另外一個測試,若是body裏面的對象缺乏某些屬性呢?(因爲對象自己只有一個屬性,我就傳遞一個無屬性對象吧- -!):

操做結果依然是沒問題的,使用GET反查一下:

name屬性就變成了null,這不難理解,PUT是總體性更新,若是傳遞的參數對象缺乏某些屬性,那麼這些屬性的值就至關因而null,也會總體更新給Model。

因爲這種緣由,PUT用的就比較少,不可能爲了更新對象中的一個屬性而把對象全部的屬性值都傳遞回去。

因此PATCH(局部更新)就應用的比較普遍了。

 

PUT不具備安全性,由於每次執行PUT都會改變資源。

可是PUT具備等冪性,這個很好理解,屢次執行同一個PUT請求後,結果是同樣的。

 

更新集合資源

跟刪除集合資源同樣,針對某個路由進行集合請求是合法的,可是這也意味着傳進來的集合要總體代替原有的集合,也就是說原有集合裏面的對象都應該刪除,而後傳進來集合的對象挨個再添加進去。可是這樣的話是有反作用的,每次執行的結果實際上是不同的。此外這種集合更新也是具備較大的破壞性,因此通常不這麼作。

 

更新或建立資源

我記得好像在使用老版本Entity Framework作種子數據的時候,常用一個擴展方法叫作AddOrUpdate(),也就是若是數據存在那就更新它,不然就建立它。

在REST API裏,咱們有時也會遇到這樣的需求。咱們暫時把這個方法叫作Upsert (Update + Insert) 。那麼問題來了應該使用POST仍是PUT呢?

PUT請求會發送到現有資源的URI上,若是資源不存在就返回404。

而POST用於建立資源,因此確定不知道該資源的URI(是指GET的URI)。

可是若是API的消費者能夠建立資源,那麼,PUT請求能夠被髮送到一個暫時不存在的資源的URI上;若是資源不存在,那就建立它,不然就修改它。

因此感受使用PUT做爲Upsert的HTTP方法比較合適一些。

可是若是使用自增類主鍵Id的話,這種狀況就不適合了。

下面咱們假設City的Id不是自增的,那麼咱們能夠這樣修改一下Update方法:

 

因爲個人例子主鍵是自增的,因此不適合Upsert。我就不測試了。

可是整體的思路就是這樣,注意裏面新增和修改返回的結果略有不一樣。 

 

PATCH 局部更新資源

使用PUT最總體更新,缺點仍是很明顯的,因此我更多使用的是PATCH局部更新。

HTTP PATCH請求的body部分須要使用RFC 6902 (JSOn Patch)這個標準來進行描述。

而PATCH請求的media type應該設定爲 "application/json-patch+json"。

PATCH請求的body是一個操做的數組

這個例子裏面有兩個操做:

第一個是「replace」操做(op的值就是操做的類型),path表明着資源的屬性名value表示的是更新後的值

第二個操做類型是「remove」,表示要刪除資源的某個屬性的值,例子裏是name屬性。

JSON PATCH的操做類型主要有六種:

  • 添加:{「op」: "add", "path": "/xxx", "value": "xxx"},若是該屬性不存,那麼就添加該屬性,若是屬性存在,就改變屬性的值。這個對靜態類型不適用。
  • 刪除:{「op」: "remove", "path": "/xxx"},刪除某個屬性,或把它設爲默認值(例如空值)。
  • 替換:{「op」: "replace", "path": "/xxx", "value": "xxx"},改變屬性的值,也能夠理解爲先執行了刪除,而後進行添加。
  • 複製:{「op」: "copy", "from": "/xxx", "path": "/yyy"},把某個屬性的值賦給目標屬性。
  • 移動:{「op」: "move", "from": "/xxx", "path": "/yyy"},把源屬性的值賦值給目標屬性,並把源屬性刪除或設成默認值。
  • 測試:{「op」: "test", "path": "/xxx", "value": "xxx"},測試目標屬性的值和指定的值是同樣的。

注意,path屬性可能具備層級結構,而value屬性也沒必要非得是字符串。

看下代碼:

傳遞進來的body參數須要使用JsonPatchDocument<T>這個類型,在這裏我把它叫作patchDoc。首先要把EFCore的City映射成CityUpdateResource,這樣這個CityUpdateResource就有了該City在數據庫裏最新的屬性值。而後經過patchDoc.ApplyTo()這個方法把patchDoc的操做依次附加給這個CityUpdateResource,這時候全部須要更新的值都體如今CityUpdateResource裏了,而該對象其它的屬性值則是數據庫裏的最新值,也就是不須要更新的值。最後再把它的值映射給EFCore的City,進行更新就能夠了。最後EFCore作的操做確定是總體更新,可是以前咱們把最新值都放在CityUpdateResource裏了,因此就至關於只作了局部更新。

測試:

請求的Content-Type應該是"application/json-patch+json",可是若是之寫成application/json好像也能夠。

 結果:

(爲了更好的測試,我又爲City添加了Description屬性)

下面remove的測試:

反查:

在測試一下多個操做:

結果就不看了,都是OK的。

 

PATCH用來局部更新或建立資源

 能夠修改相關代碼來支持局部更新或建立資源的操做:

這個我就不測試了,自增Id不適合這種操做。

 

HTTP方法適用總結

經常使用的5中HTTP方法都介紹了,下面總結一下:

GET(獲取資源):

  • GET api/countries,返回200,集合數據;找不到數據返回 404。
  • GET api/countries/{id}, 返回200,單個數據;找不到返回 404.

DELETE(刪除資源)

  • DELETE api/countries/{id},成功204;沒找到資源 404。
  • DELETE api/countries,不多用,也是204或者404.

POST (建立資源):

  • POST api/countries, 成功返回 201 和單個數據;若是資源沒有建立則返回 404
  • POST api/countries/{id},確定不會成功,返回 404或409.
  • POST api/countrycollections,成功返回 201 和集合;沒建立資源則返回 404

PUT (總體更新):

  • PUT api/countries/{id}, 成功能夠返回200,204;沒找到資源則返回 404
  • PUT api/countries,集合操做不多見,返回 200,204或404

PATCH(局部更新):

  • PATCH api/countries/{id},200單個數據,204或者404
  • PATCH api/countries, 集合操做不多見,返回 200集合,204或404.

 

驗證

爲了進行輸入驗證(不驗證輸出),咱們須要作如下三方面工做:

  • 定義驗證規則
  • 檢查驗證規則
  • 把驗證錯誤信息發送給API的消費者

以前的文章也提到的ASP.NET Core裏面定義驗證規則的方式:

  • Data annotations 數據註解,就是那種在屬性上面的中括號樣式的屬性標籤
  • 如何數據註解沒法知足要求,則可使用自定義的驗證方式
    • 能夠自定義數據註解
    • 也可讓被驗證類實現IValidatableObject接口
  • 也可使用像FluentApi這樣的第三方驗證庫

檢查驗證規則的方式:

  • 使用 ModelState
    • 它是一個字典,包含了Model的狀態以及Model所綁定的驗證
    • 對於提交的每一個屬性,它都包含了一個錯誤信息的集合
  • ModelState.IsValid(),若是出現任何一個錯誤,ModelState.IsValid屬性就會變成false。

報告驗證錯誤信息:

  • 返回的狀態嗎應該是 422 Unprocessable Entity (上文講過,422表示請求的格式沒問題,可是語義有錯誤,例如實體驗證錯誤)
  • 除了狀態碼以外,還須要把驗證錯誤信息在響應的body裏面帶回去

 

爲EFCore的Model添加約束

我以前尚未爲EFCore的model添加約束,這裏我添加上(因爲我使用的是內存數據庫,因此下面的約束是不起做用的,這些約束只有在關係型數據庫才起做用):

對於EFCore的實體約束和驗證,我不肯意使用註解的方式(由於Model類應該只幹本身的活),更喜歡使用fluent api

而後把這兩個類添加到DbContext裏面的OnModelCreating方法裏便可:

雖然上面的代碼對內存數據庫沒有用,可是我仍是添加上吧。

若是一個HTTP請求形成了EFCore model的驗證失敗,若是返回500的話,感受就不太正確。由於若是是500錯誤的話,就意味着是服務器出現了錯誤,而這其實是API消費者(客戶端)提交的數據有問題,是客戶端的錯誤。因此返回的狀態碼應該是 4xx 系列。

此外,目前這些驗證規則是處於EFCore 的實體上的,而報告給API消費者的驗證錯誤信息應該定義在Resource這一層面上,因此下面就爲Resource model定義驗證規則:

全部的驗證註解能夠查看官方文檔:https://msdn.microsoft.com/en-us/library/system.componentmodel.dataannotations(v=vs.110).aspx

(這種方式比較簡單,可是把驗證和Model混合到了一塊兒,因此不少人仍是不採用這種方式的)

驗證規則定義完了,下面來實施規則檢查。這時就須要使用ModelState了。

每當請求進入到這個方法的時候,都會驗證咱們剛剛定義在Resource上的這些約束,若是其中一個約束沒有達標,則ModelState的IsValid屬性就會是false;此外若是傳進來的屬性類型和定義的不符,IsValid屬性也會是false。

這裏返回狀態碼 422 是正確的選擇,可是 422 要求請求的body的語法必須是正確的,不能是null,因此前面檢查是否爲null的代碼還須要保留。

因爲ASP.NET Core並無內置的幫助方法能夠返回422和驗證錯誤信息,因此咱們先創建一個類用於返回 422 和驗證錯誤信息,它繼承於ObjectResult

其中的SerializableError定義了一個能夠被串行化的容器,該容器能夠以Key-Value對的形式來保存ModelState的信息。

回到CityController的POST的Action方法,只添加這部分代碼便可:

下面進行測試:

能夠看到驗證的錯誤信息都按預期返回了。

再試試另一組測試:

 

下面考慮下若是據註解沒法知足驗證要求的狀況,這時就須要寫自定義的驗證。

以前文章講過,有幾種方法能夠寫自定義驗證邏輯:

  • 自定義驗證屬性標籤(數據註解),編寫一個繼承於ValidationAttribute的類
  • 讓Resource類實現IValidatableObject接口
  • 使用FluentValidation以及相似的第三方庫
  • 直接在方法裏寫驗證邏輯

我比較傾向於後兩種方法,尤爲是第三種。可是因爲本文主要是講RESTful API相關的,因此我先避免過多的使用第三方庫,我暫時先採用第四種方法。

假設我要求City的name屬性值不能夠是「中國」:

這裏要用到ModelState的AddModelError方法。

測試:

OK.

 

下面看一下PUT的驗證。

大部分狀況下,PUT的驗證可能和POST是同樣的,可是有時仍是不同的,因此分別寫兩個ResourceModel對應POST和PUT的優點就體現出來了。

可是這兩個類的大部分代碼仍是同樣的,因此能夠採起使用抽象父類的方法來去掉重複的代碼,創建CityResource:

注意屬性必定要使用virtual關鍵字,由於在子類裏咱們可能會重寫屬性。

在這裏我把Description的Required約束去掉了。

再看CityAddResource:

繼承抽象類便可,屬性和驗證徹底同樣。

再看CityUpdateResource:

這裏,我對Description屬性添加了Required約束,而其它約束和父類保持一致。

最後修改PUT的Action方法:

測試,POST:

OK。

再測試PUT,尤爲是Description屬性:

子類裏Description的約束進行了檢查。

再測試父類裏Description的約束:

OK, 說明子類裏Description的約束和父類裏Description的約束都起做用。

在子類CityUpdateResource裏,還能夠這樣寫:

這樣或許更清晰。

 

到目前爲止,我使用的是數據註解的方式來爲ResourceModel添加驗證規則,這樣作其實不是很好,沒有關注點分離(Soc,Seperation of Concerns)

並且,咱們的自定義驗證代碼也是處處重複的寫,這樣也不對。

因此儘管數據註解看起來很簡單,少寫了一些代碼,可是開發軟件應該更加註重可維護性,要儘可能遵循那些設計原則,適當使用設計模式,寫單元測試和E2E測試,儘管這樣會形成看起來多寫了一些代碼,可是考慮到軟件的質量以及更重要的後期維護,實際上這樣作是大大的節省了成本。綜上緣由,我推薦使用第三方庫,FluentValidationhttps://github.com/JeremySkinner/FluentValidation

使用FluentValidation

安裝FluentValidation,能夠經過Nuget,Package Manager Console 或者 .net cli:

直接安裝這個就能夠:

而後會自動安裝依賴的庫:

把那些ResourceModel的數據註解驗證約束都去掉,把Controller裏面自定義驗證的代碼也去掉,而後爲每個類添加一個驗證器Validator:

首先是Country的,這個簡單:

其中大括號裏面的字符串是參數(佔位符),{PropertyName}就是屬性的名字若是使用了WithName()方法,那就是WithName裏面設定的別名;{MaxLength}就是指設定的最大長度約束的值。有不少這種佔位符,仍是須要看官方文檔。

下面看看City相關的驗證,這裏有個繼承的關係,首先是把共有的驗證提取出來做爲父類:

 

這裏使用泛型比較好。

而後CityUpdateResource:

 

因爲父子關係,父類的構造函數先執行,而後執行CityUpdateResourceValidator的構造函數。

 

最後還要爲ASP.NET Core配置FluentValidation,在Startup的ConfigureServices方法裏:

首先使用擴展方法AddFluentValidation();而後爲每個Resource Model 配置驗證器。若是你不想挨個添加配置驗證器的話,可使用:

來把某個Assembly裏的驗證器所有添加進來,可是我仍是比較喜歡一個一個寫,重構的時候有什麼錯誤能當即發現,可是也容易忘記添加。

而後測試一下,效果和以前是同樣的。

使用FluentValidation,作到了很好的分離,我我的感受很是好,雖然多寫了些代碼,可是更靈活,也更易於維護。

 

PATCH的驗證

PATCH與POST和PUT的驗證稍微有一點不一樣,首先看一個例子,刪除一個不存在的屬性的值:

這個會致使返回500錯誤,這是不對的。

這時,可已使用patchDoc.ApplyTo的一個重載方法,它能夠接受ModelState做爲參數,因此patchDoc裏面有任何驗證錯誤都會在ModelState裏面體現出來,(注意是PatchDoc的驗證錯誤而不是CityUpdateResource)

而後從新測試:

 

我以前已經設定了CityUpdateResource的Description屬性是必填的,那我再作一個PATCH測試,把該屬性的值去掉(設爲null):

它返回了 204, 也就是說被成功的執行了,那麼確定是有些地方沒有作約束檢查遺漏了。

由於咱們只檢查了patchDoc,而沒有檢查手動創建的那個CityUpdateResource(cityToPatch),因此這裏可已使用TryValidateModel(xx),來手動檢查cityToPatch:

測試:

此次OK了。

 

Log

在預備知識文章裏,我已經介紹了Log相關的內容,因此這裏就再也不重複敘述了(http://www.javashuo.com/article/p-xdbrkauk-e.html)。

看咱們以前寫的捕獲異常的代碼,在Startup的Configure方法裏:

如今的代碼是爲API的消費者返回了500狀態碼,並返回了一些錯誤信息。這樣作咱們就把異常信息給丟掉了,可是又不該該把異常信息傳遞給API消費者,而咱們確實須要這個異常信息,因此咱們把異常記錄到日誌。

有多種方式能夠獲得Logger,這裏我使用ILoggerFactory:

而後在Configure方法裏面相應的位置建立Logger並記錄日誌:

整個應用的日誌仍是作分類比較好,這裏我使用LoggerFactory的CreateLogger方法建立了Logger,其分類是「Global Exception Logger」。

這裏使用了500做爲Log的EventId比較合適,畢竟是500錯誤。

我認爲能夠把Action裏面返回500狀態碼的部分改爲拋出異常。

而後我修改一下PATCH,以便能拋出一個異常:

測試:

異常被正常的拋出,在看一下控制檯的Log:

Log信息也被正確的打印。

 

下面在看看如何在Controller裏面記錄日誌,首先注入Logger:

ILogger<T>,T就是日誌分類的名字,這裏建議使用Controller的名字。

而後在Action里正常記錄日誌就能夠了:

就不測試了。

 

使用Serilog

在實際應用中只把日誌記錄到控制檯或Debug窗口是沒用的,最好的辦法仍是記錄到文件或者數據庫等。

支持ASP.NET Core的第三方Log提供商有不少,NLog,Serilog等等。這裏我使用Serilog(https://github.com/serilog/serilog)。

Nuget安裝:

提示安裝的依賴:

而後在Program.cs裏使用擴展方法UseSerilog()使用Serilog便可,我就不作其它配置了:

Serilog支持把日誌寫入到各類的Sinks裏,能夠把sink看作媒介(文件,數據庫等)。

我須要寫入到文件,那麼就安裝:

Serilog的配置信息是這樣寫的,能夠把它放到程序比較靠前執行的地方:

這裏配置的意思是:全局最低記錄日誌級別是Debug,可是針對以Microsoft開頭的命名空間的最低級別是Information。

使用Enruch.FromLogContext()可讓程序在執行上下文時動態添加或移除屬性(這個須要看文檔)。

按日生成記錄文件,日誌文件名後會帶着日期,並放到./logs目錄下。

這就是生成的日誌文件:

注意使用了其它Log提供商以後,在它以前配置的Log提供商就不起做用了,因此控制檯不輸出Log的異常信息了:

因此仍是爲Serilog添加一個控制檯的Sink吧:

這樣控制檯和文件的Log均可以輸出了:(注意windows下的命令行有時候會卡住,須要按一下回車才能繼續)

 

此次就寫到這裏,下次寫一些翻頁和過濾的東西。

完成後的源碼:https://github.com/solenovex/ASP.NET-Core-2.0-RESTful-API-Tutorial

相關文章
相關標籤/搜索