Github源碼地址是: https://github.com/solenovex/Building-asp.net-core-2-web-api-starter-template-from-scratchhtml
本文講的是裏面的Step 2.git
上一次, 咱們使用asp.net core 2.0 創建了一個Empty project, 而後作了一些基本的配置, 並創建了兩個Controller, 寫了一些查詢方法.github
下面咱們繼續:web
POST
POST通常用來表示建立資源, 也就是新增.json
先看看Model, 其中的Id屬性, 通常是建立的時候服務器自動生成的, 因此若是客戶端在進行Post(建立)的時候, 它是不會提供Id屬性的.api
public class Product { public int Id { get; set; } public string Name { get; set; } public float Price { get; set; } public ICollection<Material> Materials { get; set; } }
因此, 能夠這樣作, 再創建一個Dto, 專門用於建立: ProductCreation.cs: 服務器
namespace CoreBackend.Api.Dtos { public class ProductCreation { public string Name { get; set; } public float Price { get; set; } } }
這裏去掉了Id和Materials這個導航屬性.app
其實也可使用同一個Model來作全部的操做, 由於它們的大部分屬性都是相同的, 可是,框架
仍是建議針對查詢, 建立, 修改, 使用單獨的Model, 這樣之後修改和重構會簡單一些, 再說他們的驗證也是不同的.asp.net
建立Post Action
[Route("{id}", Name = "GetProduct")] public IActionResult GetProduct(int id) { var product = ProductService.Current.Products.SingleOrDefault(x => x.Id == id); if (product == null) { return NotFound(); } return Ok(product); } [HttpPost] public IActionResult Post([FromBody] ProductCreation product) { if (product == null) { return BadRequest(); } var maxId = ProductService.Current.Products.Max(x => x.Id); var newProduct = new Product { Id = ++maxId, Name = product.Name, Price = product.Price }; ProductService.Current.Products.Add(newProduct); return CreatedAtRoute("GetProduct", new { id = newProduct.Id }, newProduct); }
[HttpPost] 表示請求的謂詞是Post. 加上Controller的Route前綴, 那麼訪問這個Action的地址就應該是: 'api/product'
後邊也能夠跟着自定義的路由地址, 例如 [HttpPost("create")], 那麼這個Action的路由地址就應該是: 'api/product/create'.
[FromBody] , 請求的body裏面包含着方法須要的實體數據, 方法須要把這個數據Deserialize成ProductCreation, [FromBody]就是幹這些活的.
客戶端程序可能會發起一個Bad的Request, 致使數據不能被Deserialize, 這時候參數product就會變成null. 因此這是一個客戶端發生的錯誤, 程序爲讓客戶端知道是它引發了錯誤, 就應該返回一個Bad Request 400 (Bad Request表示客戶端引發的錯誤)的 Status Code.
傳遞進來的model類型是 ProductCreation, 而咱們最終操做的類型是Product, 因此須要進行一個Map操做, 目前仍是挨個屬性寫代碼進行Map吧, 之後會改爲Automapper.
返回 CreatedAtRoute: 對於POST, 建議的返回Status Code 是 201 (Created), 可使用CreatedAtRoute這個內置的Helper Method. 它能夠返回一個帶有地址Header的Response, 這個Location Header將會包含一個URI, 經過這個URI能夠找到咱們新建立的實體數據. 這裏就是指以前寫的GetProduct(int id)這個方法. 可是這個Action必須有一個路由的名字才能夠引用它, 因此在GetProduct方法上的Route這個attribute裏面加上Name="GetProduct", 而後在CreatedAtRoute方法第一個參數寫上這個名字就能夠了, 儘管進行了引用, 可是Post方法走完的時候並不會調用GetProduct方法. CreatedAtRoute第二個參數就是對應着GetProduct的參數列表, 使用匿名類便可, 最後一個參數是咱們剛剛建立的數據實體.
運行程序試驗一下, 注意須要在Headers裏面設置Content-Type: application/json. 結果如圖:
返回的狀態是201.
看一下那一堆Headers:
裏面的location 這個Header, 因此客戶端就知道之後想找這個數據, 就須要訪問這個地址, 咱們能夠如今就試試:
嗯. 沒什麼問題.
Validation 驗證
針對上面的Post方法, 若是請求沒有Body, 參數product就會是null, 這個咱們已經判斷了; 若是body裏面的數據所包含的屬性在product中不存在, 那麼這個屬性就會被忽略.
可是若是body數據的屬性有問題, 好比說name沒有填寫, 或者name太長, 那麼在執行action方法的時候就會報錯, 這時候框架會自動拋出500異常, 表示是服務器的錯誤, 這是不對的. 這種錯誤是由客戶端引發的, 因此須要返回400 Bad Request錯誤.
驗證Model/實體, asp.net core 內置可使用 Data Annotations進行:
using System; using System.ComponentModel.DataAnnotations; namespace CoreBackend.Api.Dtos { public class ProductCreation { [Display(Name = "產品名稱")] [Required(ErrorMessage = "{0}是必填項")] // [MinLength(2, ErrorMessage = "{0}的最小長度是{1}")] // [MaxLength(10, ErrorMessage = "{0}的長度不能夠超過{1}")]
[StringLength(10, MinimumLength = 2, ErrorMessage = "{0}的長度應該不小於{2}, 不大於{1}")] public string Name { get; set; } [Display(Name = "價格")] [Range(0, Double.MaxValue, ErrorMessage = "{0}的值必須大於{1}")] public float Price { get; set; } } }
這些Data Annotation (理解爲用於驗證的註解), 能夠在System.ComponentModel.DataAnnotation找到, 例如[Required]表示必填, [MinLength]表示最小長度, [StringLength]能夠同時驗證最小和最大長度, [Range]表示數值的範圍等等不少.
[Display(Name="xxx")]的用處是, 給屬性起一個比較友好的名字.
其餘的驗證註解都有一個屬性叫作ErrorMessage (string), 表示若是驗證失敗, 就會把ErrorMessage的內容添加到錯誤結果裏面去. 這個ErrorMessage可使用參數, {0}表示Display的Name屬性, {1}表示當前註解的第一個變量, {2}表示當前註解的第二個變量.
在Controller裏面添加驗證邏輯:
[HttpPost] public IActionResult Post([FromBody] ProductCreation product) { if (product == null) { return BadRequest(); } if (!ModelState.IsValid) { return BadRequest(ModelState); } var maxId = ProductService.Current.Products.Max(x => x.Id); var newProduct = new Product { Id = ++maxId, Name = product.Name, Price = product.Price }; ProductService.Current.Products.Add(newProduct); return CreatedAtRoute("GetProduct", new { id = newProduct.Id }, newProduct); }
ModelState: 是一個Dictionary, 它裏面是請求提交到Action的Name和Value的對們, 一個name對應着model的一個屬性, 它也包含了一個針對每一個提交的屬性的錯誤信息的集合.
每次請求進到Action的時候, 咱們在ProductCreationModel添加的那些註解的驗證, 就會被檢查. 只要其中有一個驗證沒經過, 那麼ModelState.IsValid屬性就是False. 能夠設置斷點查看ModelState裏面都有哪些東西.
若是有錯誤的話, 咱們能夠把ModelState看成Bad Request的參數一塊兒返回到前臺.
咱們試試:
若是經過Data Annotation的方式不能實現比較複雜驗證的需求, 那就須要寫代碼了. 這時, 若是驗證失敗, 咱們能夠錯誤信息添加到ModelState裏面,
if (product.Name == "產品") { ModelState.AddModelError("Name", "產品的名稱不能夠是'產品'二字"); }
看看運行結果:
Good.
可是這種經過註解的驗證方式把驗證的代碼和Model的代碼混到了一塊兒, 並非很好的Separationg of Concern, 並且同時在Model和Controller裏面爲Model寫驗證相關的代碼也不太好.
這是方式是asp.net core 內置的, 因此簡單的狀況下仍是能夠用的. 若是需求比較複雜, 可使用FluentValidation, 之後會加入這個庫.
PUT
put應該用於對model進行完整的更新.
首先最好仍是單獨爲Put寫一個Dto Model, 儘管屬性可能都是同樣的, 可是也建議這樣寫, 實在不想寫也能夠.
ProducModification.cs
public class ProductModification { [Display(Name = "產品名稱")] [Required(ErrorMessage = "{0}是必填項")] [StringLength(10, MinimumLength = 2, ErrorMessage = "{0}的長度應該不小於{2}, 不大於{1}")] public string Name { get; set; } [Display(Name = "價格")] [Range(0, Double.MaxValue, ErrorMessage = "{0}的值必須大於{1}")] public float Price { get; set; } }
而後編寫Controller的方法:
[HttpPut("{id}")] public IActionResult Put(int id, [FromBody] ProductModification product) { if (product == null) { return BadRequest(); } if (product.Name == "產品") { ModelState.AddModelError("Name", "產品的名稱不能夠是'產品'二字"); } if (!ModelState.IsValid) { return BadRequest(ModelState); } var model = ProductService.Current.Products.SingleOrDefault(x => x.Id == id); if (model == null) { return NotFound(); } model.Name = product.Name; model.Price = product.Price; // return Ok(model); return NoContent(); }
按照Http Put的約定, 須要一個id這樣的參數, 用於查找現有的model.
因爲Put作的是完整的更新, 因此把ProducModification整個Model做爲參數.
進來以後, 進行了一套和POST一摸同樣的驗證, 這地方確定能夠改進, 若是驗證邏輯比較複雜的話, 處處寫一樣驗證邏輯確定是很差的, 因此建議使用FluentValidation.
而後, 把ProductModification的屬性都映射查詢找到給Product, 這個之後用AutoMapper來映射.
返回: PUT建議返回NoContent(), 由於更新是客戶端發起的, 客戶端已經有了最新的值, 無需服務器再給它傳遞一次, 固然了, 若是有些值是在後臺更新的, 那麼也可使用Ok(xxx)而後把更新後的model做爲參數一塊兒傳到前臺.兩種效果如圖:
注意: PUT是總體更新/修改, 可是若是隻想修改部分屬性的時候, 咱們看看會發生什麼.
首先在Product相關Dto裏面再加上一個屬性Description吧.
而後在POST和PUT的方法裏面映射那部分, 添加上相應的代碼, (若是有AutoMapper, 這不操做就不須要作了):
而後咱們用PUT進行實驗單個屬性修改:
這對這條數據:
咱們修改name和price屬性:
而後再看一下修改後的數據:
Description被設置成null. 這就是HTTP PUT標準的本意: 總體修改, 更新全部屬性, 儘管你的代碼可能不這麼作.
Patch 部分更新
Http Patch 就是作部分更新的, 它的Request Body應該包含須要更新的屬性名 和 值, 甚至也能夠包含針對這個屬性要進行的相應操做.
針對Request Body這種狀況, 有一個標準叫作 Json Patch RFC 6092, 它定義了一種json數據的結構 能夠表示上面說的那些東西.
Json Patch定義的操做包含替換, 複製, 移除等操做.
這對咱們的Product, 它的結構應該是這樣的:
op 表示操做, replace 是指替換; path就是屬性名, value就是值.
相應的Patch方法:
[HttpPatch("{id}")] public IActionResult Patch(int id, [FromBody] JsonPatchDocument<ProductModification> patchDoc) { if (patchDoc == null) { return BadRequest(); } var model = ProductService.Current.Products.SingleOrDefault(x => x.Id == id); if (model == null) { return NotFound(); } var toPatch = new ProductModification { Name = model.Name, Description = model.Description, Price = model.Price }; patchDoc.ApplyTo(toPatch, ModelState); if (!ModelState.IsValid) { return BadRequest(ModelState); } model.Name = toPatch.Name; model.Description = toPatch.Description;
model.Price = toPatch.Price; return NoContent(); }
HttpPatch, 按約定方法有一個參數id, 還有一個JsonPatchDocument類型的參數, 它的泛型應該是用於Update的Dto, 因此選擇的是ProductionModification. 若是使用Product這個Dto的話, 那麼它包含id屬性, 而id屬性是不更改的. 但若是你沒有針對不一樣的操做使用不一樣的Dto, 那麼別忘了檢查傳入Dto的id 要和參數id一致才行.
而後把查詢出來的product轉化成用於更新的ProductModification這個Dto, 而後應用於Patch Document 就是指爲toPatch這個model更新那些須要更新的屬性, 是使用ApplyTo方法實現的.
可是這時候可能會出錯, 好比說修改一個根本不存在的屬性, 也就是說客戶端可能引發了錯誤, 這時候就須要它進行驗證, 並返回Bad Request. 因此就加上ModelState這個參數. 而後進行判斷便可.
而後就是和PUT同樣的更新操做, 把toPatch這個Update的Dto再總體更新給model. 其實裏面無論怎麼實現, 只要按約定執行就好.
而後按建議, 返回NoContent().
試一下:
而後查詢一下:
與期待的結果同樣.
而後試一下傳入一個不存在的屬性:
結果顯示找不到這個屬性.
再試一下, ProductModification 這個model上的驗證: 例如刪除name這個屬性的值:
返回204, 表示成功, 可是name是必填的, 因此代碼還有問題.
咱們作了ModelState檢查, 可是爲何沒有驗證出來呢? 這是由於, Patch方法的Model參數是JsonPatchDocument而不是ProductModification, 上面傳進去的參數對於JsonPatchDocument來講是沒有問題的.
因此咱們須要對toPatch這個model進行驗證:
[HttpPatch("{id}")] public IActionResult Patch(int id, [FromBody] JsonPatchDocument<ProductModification> patchDoc) { if (patchDoc == null) { return BadRequest(); } var model = ProductService.Current.Products.SingleOrDefault(x => x.Id == id); if (model == null) { return NotFound(); } var toPatch = new ProductModification { Name = model.Name, Description = model.Description, Price = model.Price }; patchDoc.ApplyTo(toPatch, ModelState); if (!ModelState.IsValid) { return BadRequest(ModelState); } if (toPatch.Name == "產品") { ModelState.AddModelError("Name", "產品的名稱不能夠是'產品'二字"); } TryValidateModel(toPatch); if (!ModelState.IsValid) { return BadRequest(ModelState); } model.Name = toPatch.Name; model.Description = toPatch.Description; model.Price = toPatch.Price; return NoContent(); }
使用TryValidateModel(xxx)對model進行手動驗證, 結果也會反應在ModelState裏面.
再試一次上面的操做:
這回對了.
DELETE 刪除
這個比較簡單:
[HttpDelete("{id}")] public IActionResult Delete(int id) { var model = ProductService.Current.Products.SingleOrDefault(x => x.Id == id); if (model == null) { return NotFound(); } ProductService.Current.Products.Remove(model); return NoContent(); }
按Http Delete約定, 參數爲id, 若是操做成功就回NoContent();
試一下:
成功.
目前, CRUD最基本的操做先告一段落.
上班了比較忙了, 今天先寫這些.....................................................
轉自:http://www.cnblogs.com/cgzl/p/7640077.html