上一篇是: http://www.cnblogs.com/cgzl/p/7637250.html
Github源碼地址是: https://github.com/solenovex/Building-asp.net-core-2-web-api-starter-template-from-scratch
本文講的是里面的Step 2.
上一次, 我們使用asp.net core 2.0 建立了一個Empty project, 然后做了一些基本的配置, 並建立了兩個Controller, 寫了一些查詢方法.
下面我們繼續:
POST
POST一般用來表示創建資源, 也就是新增.
先看看Model, 其中的Id屬性, 一般是創建的時候服務器自動生成的, 所以如果客戶端在進行Post(創建)的時候, 它是不會提供Id屬性的.
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這個導航屬性.
其實也可以使用同一個Model來做所有的操作, 因為它們的大部分屬性都是相同的, 但是,
還是建議針對查詢, 創建, 修改, 使用單獨的Model, 這樣以后修改和重構會簡單一些, 再說他們的驗證也是不一樣的.
創建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吧.

namespace CoreBackend.Api.Dtos { public class Product { public int Id { get; set; } public string Name { get; set; } public float Price { get; set; } public string Description { get; set; } public ICollection<Material> Materials { get; set; } } } namespace CoreBackend.Api.Dtos { public class ProductCreation { [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; } [Display(Name = "描述")] [MaxLength(100, ErrorMessage = "{0}的長度不可以超過{1}")] public string Description { get; set; } } } namespace CoreBackend.Api.Dtos { 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; } [Display(Name = "描述")] [MaxLength(100, ErrorMessage = "{0}的長度不可以超過{1}")] public string Description { get; set; } } }
然后在POST和PUT的方法里面映射那部分, 添加上相應的代碼, (如果有AutoMapper, 這不操作就不需要做了):

[HttpPost] public IActionResult Post([FromBody] ProductCreation product) { if (product == null) { return BadRequest(); } if (product.Name == "產品") { ModelState.AddModelError("Name", "產品的名稱不可以是'產品'二字"); } 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, Description = product.Description }; ProductService.Current.Products.Add(newProduct); return CreatedAtRoute("GetProduct", new { id = newProduct.Id }, newProduct); } [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; model.Description = product.Description; return NoContent(); }
然后我們用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最基本的操作先告一段落.
上班了比較忙了, 今天先寫這些.....................................................