Github源码地址是: https://github.com/solenovex/Building-asp.net-core-2-web-api-starter-template-from-scratchjavascript
本文讲的是里面的Step 2.html
上一次, 咱们使用asp.net core 2.0 创建了一个Empty project, 而后作了一些基本的配置, 并创建了两个Controller, 写了一些查询方法.java
下面咱们继续:git
POST
POST通常用来表示建立资源, 也就是新增.github
先看看Model, 其中的Id属性, 通常是建立的时候服务器自动生成的, 因此若是客户端在进行Post(建立)的时候, 它是不会提供Id属性的.web
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: json
namespace CoreBackend.Api.Dtos { public class ProductCreation { public string Name { get; set; } public float Price { get; set; } } }
这里去掉了Id和Materials这个导航属性.api
其实也可使用同一个Model来作全部的操做, 由于它们的大部分属性都是相同的, 可是,服务器
仍是建议针对查询, 建立, 修改, 使用单独的Model, 这样之后修改和重构会简单一些, 再说他们的验证也是不同的.app
建立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最基本的操做先告一段落.
上班了比较忙了, 今天先写这些.....................................................
转自:http://www.cnblogs.com/cgzl/p/7640077.html