【轉載】從頭編寫 asp.net core 2.0 web api 基礎框架 (2) 【轉載】從頭編寫 asp.net core 2.0 web api 基礎框架 (2)

【轉載】從頭編寫 asp.net core 2.0 web api 基礎框架 (2)

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吧.

  View Code

而後在POST和PUT的方法裏面映射那部分, 添加上相應的代碼, (若是有AutoMapper, 這不操做就不須要作了):

  View Code

而後咱們用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

相關文章
相關標籤/搜索