从头编写 asp.net core 2.0 web api 基础框架 (2)

上一篇是: 从头编写 asp.net core 2.0 web api 基础框架 (1)

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

 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最基本的操作先告一段落.

相关文章:

  • 从头编写 asp.net core 2.0 web api 基础框架 (1)

  • Asp.Net Core 2.0 多角色权限认证

  • 调试 ASP.NET Core 2.0 源代码

原文地址: http://www.cnblogs.com/sheng-jie/p/7640163.html 


.NET社区新闻,深度好文,微信中搜索dotNET跨平台或扫描二维码关注

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/323335.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

nssl1217-So many prefix?【KMP】

正题 题目大意 求长度为偶数的前缀在字符串SSS中出现的次数和。 解题思路 我们先不考虑长度为偶数的话&#xff0c;答案很好求。先求出KMP的next数组&#xff0c;然后numinumnexti1num_inum_{next_i}1numi​numnexti​​1。 之后num的和就是答案。 注:num数组表示前i个字符的…

js中toString()和String()区别详解

转载自 js中toString()和String()区别详解 我们知道String()与 .toString()都是可以转换为字符串类型&#xff0c;但是String()与 .toString()的还是有区别的 1、.toString()可以将所有的的数据都转换为字符串&#xff0c;但是要排除null 和 undefined 例如将false转为字符…

java实现上传网络图片到七牛云存储

大家好&#xff0c;我是雄雄。 前言 最近阳了&#xff0c;第二条杠红的发紫&#xff0c;真难受啊&#xff0c;但是吧&#xff0c;博客上有个bug&#xff0c;不解决感觉比阳了还难受。 话还是要从博客的图片显示不出来这里说起&#xff0c;当时做的时候&#xff0c;在发文章这…

【php】php对mysql的连接操作【mysql】

思路&#xff1a; 1、数据库做两个表单&#xff0c;一个是user用来记录用户的信息&#xff0c;方便登录与注册。另一个表单是chat&#xff0c;用来记录聊天内容。 2、用到的技术是ajax&#xff0c;网页及时交互数据&#xff0c;可以做到无刷新聊天。 遍历数据库表单 数据库连…

nssl1218-TRAVEL【SPFA】

正题 题目大意 n个图&#xff0c;有m条双向道路&#xff0c;每条道路有一个l和r。 求一条路径&#xff0c;使得路上最小的r和路上最大的l的差最大。 解题思路 我们考虑枚举l&#xff0c;然后用SPFA计算最大的r。然后这样会超时。 之后我们发现其实答案的l一定是某一条边的l&…

gRPC官方快速上手学习笔记(c#版)

上手前准备工作 支持操作系统&#xff1a;windows、OS X、Linux。实例采用.net、.net core sdk。 The .NET Core SDK command line tools. The .NET framework 4.5 (for OS X and Linux, the open source .NET Framework implementation, “Mono”, at version 4, is suitable…

JS中的加号+运算符详解

转载自 JS中的加号运算符详解 加号运算符 在 JavaScript 中&#xff0c;加法的规则其实很简单&#xff0c;只有两种情况: 把数字和数字相加把字符串和字符串相加 所有其他类型的值都会被自动转换成这两种类型的值。 为了能够弄明白这种隐式转换是如何进行的&#xff0c;我们…

ajax做聊天交互

本想学了几天ajax就可以弄一个类似于qq的网页聊天界面&#xff0c;发现自己想的还是太简单了。 有两个问题无法解决&#xff1a; 1、即使用ajax还是无法保证数据的及时交互&#xff0c;没有办法无时无刻刷新页面。现学的ajax还是依赖于点击事件&#xff0c;才能请求后台数据。 …

ssl提高组周三备考赛【2018.10.24】

前言 快乐题警告&#xff01; 成绩 RankRankRankPersonPersonPersonScoreScoreScoreAAABBBCCC1112017myself2017myself2017myself2102102101001001001001001001010102222017zyc2017zyc2017zyc1581581581001001001818184040403332017xxy2017xxy2017xxy157157157100100100272727…

spring cloud+dotnet core搭建微服务架构:Api网关(三)

前言 国庆假期&#xff0c;一直没有时间更新。 根据群里面的同学的提问&#xff0c;强烈推荐大家先熟悉下spring cloud。文章下面有纯洁大神的spring cloud系列。 上一章最后说了&#xff0c;因为服务是不对外暴露的&#xff0c;所以在外网要访问服务必须通过API网关来完成&…

java实现人脸识别源码【含测试效果图】——前期准备工作及访问提示

注意&#xff1a; 看完之后如有不懂&#xff0c;请看&#xff1a;关于人脸和指纹识别共同交流方案&#xff0c;也可以关注微信公众号&#xff1a;雄雄的小课堂&#xff0c;回复&#xff1a;人脸识别群获取群号&#xff0c;群内有直接可以运行的源码可供下载&#xff0c;人脸识…

JS原型链与instanceof底层原理

转载自 JS原型链与instanceof底层原理 一、问题&#xff1a; instanceof 可以判断一个引用是否属于某构造函数&#xff1b; 另外&#xff0c;还可以在继承关系中用来判断一个实例是否属于它的父类型。 老师说&#xff1a;instanceof的判断逻辑是&#xff1a; 从当前引用的…

P2522-[HAOI2011]Problem b【莫比乌斯反演】

正题 题目大意 求∑iab∑jcd(gcd(i,j)k)\sum_{ia}^b\sum_{jc}^d(gcd(i,j)k)ia∑b​jc∑d​(gcd(i,j)k) 解题思路 定义 f(i)∑i1n∑j1m(gcd(i,j)i)f(i)\sum_{i1}^n\sum_{j1}^m(gcd(i,j)i)f(i)i1∑n​j1∑m​(gcd(i,j)i) 然后计算f利用容斥计算答案 之后我们考虑如何计算 F(i)…

正则之注册登录

不久前写了个登录注册的网站&#xff0c;因为未对其做出限制&#xff0c;所以&#xff0c;随便你输入什么都可以注册成功&#xff0c;遂想怎么通过js规定注册的账号 我的要求是&#xff1a; 一&#xff1a;输入框不能为空&#xff0c;不能太长也不能太短 二&#xff1a; 1、注…

猿创征文|从酒店前台收银到软件研发教学主管到技术经理之路~

大家好&#xff0c;我是雄雄。 内容先知&#x1f449;前言☝酒店收银&#x1f928;项目组长&#x1f91c;OA管理系统&#x1f91c;酒店管理系统&#x1f468;‍&#x1f3eb;软件研发讲师&#x1f4cc;学术主管&#x1f468;‍&#x1f4bb;技术经理&#x1f449;项目情况&…

微服务~分布式事务里的最终一致性

本地事务ACID大家应该都知道了&#xff0c;统一提交&#xff0c;失败回滚&#xff0c;严格保证了同一事务内数据的一致性&#xff01;而分布式事务不能实现这种ACID&#xff0c;它只能实现CAP原则里的某两个&#xff0c;CAP也是分布式事务的一个广泛被应用的原型&#xff0c;CA…

USACO2.4の其中3道水题【模拟,图论】

T1:P1518-两只塔姆沃斯牛 The Tamworth Two 题目大意 两个东西&#xff0c;按照一个方向前进&#xff0c;他们撞到墙壁会顺时针90&#xff0c;求他们多久后相遇。 解题思路 暴力模拟 code // luogu-judger-enable-o2 #include<cstdio> #include<iostream> using…

JavaFX仿windows文件管理器目录树

一、windows文件管理器目录树 二、代码 /** To change this license header, choose License Headers in Project Properties.* To change this template file, choose Tools | Templates* and open the template in the editor.*/ package cn.util;import imagemanagesystem.…

用正则判断字符串是否为中文的方法

检测是否为中文 var reg /^([\u4E00-\u9FA5])*$/; if (!reg.test(name)) 好看字体 <!DOCTYPE html><html><head><meta charset"UTF-8"><title></title></head><body><h1 class"vintage1">美丽的…

开源纯C#工控网关+组态软件(三)加入一个新驱动:西门子S7

一、 引子 首先感谢博客园&#xff1a;第一篇文章、第一个开源项目&#xff0c;算是旗开得胜。可以看到&#xff0c;项目大部分流量来自于博客园&#xff0c;码农乐园&#xff0c;名不虚传^^。 园友给了我很多支持&#xff0c;并提出了很好的改进意见。现加入屏幕分辨率自适应…