用ASP.NET Core 2.0 创建规范的 REST API -- DELETE, UPDATE, PATCH 和 Log

本文所需的一些预备知识能够看这里: http://www.cnblogs.com/cgzl/p/9010978.html 和 http://www.cnblogs.com/cgzl/p/9019314.htmlhtml

创建Richardson成熟度2级的POST和 GET的RESTful API请看这里:http://www.javashuo.com/article/p-sryarofa-ct.htmlgit

以前一篇文章介绍了POST和GET,这篇要介绍创建Richardson成熟度2级的DELETE, PUT, PATCH.github

本文须要用到的代码(右键另存,后缀改成zip): https://images2018.cnblogs.com/blog/986268/201805/986268-20180524161857994-217513181.jpg数据库

DELETE 删除资源

这个很简单,以删除City为例:json

首先查找Country,没找到就返回404 Not Found;而后查找City,没找到也返回 404 Not Found;若是找到了,删除保存的时候失败,则返回 500 Internal Server Error;若是删除成功,则不须要返回什么内容,返回204 No Content便可。windows

测试:设计模式

若是再次执行该请求的话,不出意外的会返回 404 Not Found:api

DELETE并不具备安全性,由于在方法执行后会改变资源(把资源删除了)。数组

可是DELETE是具备幂等性的,这个你可能会有疑问,我执行屡次DELETE后返回的状态码不同为何还具备幂等性。安全

以前我提过幂等性的简单定义,那个定义多少有点模糊,咱们再来看一下幂等性定义里关键的一句话:“the side-effects of N > 0 identical requests is the same as for a single request”,意思是屡次请求的反作用和单次请求的反作用是同样的。幂等性的核心概念能够理解为:"你能够发送多于一次的一样请求,可是不会对服务器形成额外的改变"。也就是说每次发送了DELETE请求以后,服务器的状态都是同样的

 

一块儿删除主从资源

这种状况也很常见,在删除Country资源的同时,把它的子资源City也删掉。

这个很简单,因为EFCore作了不少工做,就不须要在删除主资源的时候手动去删除它全部的子资源了。

测试:

 

删除集合资源

DELETE "http://localhost:5000/api/countries",这个请求是合理的。可是确实不多这么作,由于这么作的破坏性仍是挺大的。。。

 

PUT 更新资源

Put应该用来对资源的总体更新

因为PUT是对资源的总体修改,请求body中应该带着更新对象,因此先创建这个对象:

自己City这个Model就只有两个字段,而id的应该做为路由的参数传递进来,因此在CityUpdateResource里面就不须要id属性了;若是有Id的话,你可能还要与路由参数里的id进行比较,若是不一样会带来麻烦,因此这个对象里不带id。

这时你也能够发现CityUpdateResource和CityAddResource所含有的属性是同样的,那么为何不使用同一个类型呢?由于这两个对象的目的不一样,责任不一样,一个类只应该有一个责任(SRP)。可是你可使用某个父类把相同的属性抽取出去,而后分别继承,可是我就不这样作了。

下面看这个PUT的Action方法:

这个方法也很简单,其中有两点须要注意:怎么把传递进来的对象的全部属性值都传递给EFCore的Model?这里使用AutoMapper便可,上面红框的方法就是把第一个参数对象的属性映射到第二个参数对象上。

再有就是应该返回什么?我认为Ok和NoContent都是能够的,若是在Action的方法里某些属性的值是在这里改变的,那么可使用Ok把最新的对象传递回去;可是若是在Action方法里没有再修改其它属性的值,也就是说更新以后和传递进来的对象的属性值是同样的,那就没有必要再把最新的对象传递回去了,这时就应该使用NoContent。

再看一下Repository里面:

注意这个是DbContext的方法而不是DbSet的方法,它会追踪city,而后把它的ModelState设置为Modified。

测试:

OK.

下面作另外一个测试,若是body里面的对象缺乏某些属性呢?(因为对象自己只有一个属性,我就传递一个无属性对象吧- -!):

操做结果依然是没问题的,使用GET反查一下:

name属性就变成了null,这不难理解,PUT是总体性更新,若是传递的参数对象缺乏某些属性,那么这些属性的值就至关因而null,也会总体更新给Model。

因为这种缘由,PUT用的就比较少,不可能为了更新对象中的一个属性而把对象全部的属性值都传递回去。

因此PATCH(局部更新)就应用的比较普遍了。

 

PUT不具备安全性,由于每次执行PUT都会改变资源。

可是PUT具备等幂性,这个很好理解,屡次执行同一个PUT请求后,结果是同样的。

 

更新集合资源

跟删除集合资源同样,针对某个路由进行集合请求是合法的,可是这也意味着传进来的集合要总体代替原有的集合,也就是说原有集合里面的对象都应该删除,而后传进来集合的对象挨个再添加进去。可是这样的话是有反作用的,每次执行的结果实际上是不同的。此外这种集合更新也是具备较大的破坏性,因此通常不这么作。

 

更新或建立资源

我记得好像在使用老版本Entity Framework作种子数据的时候,常用一个扩展方法叫作AddOrUpdate(),也就是若是数据存在那就更新它,不然就建立它。

在REST API里,咱们有时也会遇到这样的需求。咱们暂时把这个方法叫作Upsert (Update + Insert) 。那么问题来了应该使用POST仍是PUT呢?

PUT请求会发送到现有资源的URI上,若是资源不存在就返回404。

而POST用于建立资源,因此确定不知道该资源的URI(是指GET的URI)。

可是若是API的消费者能够建立资源,那么,PUT请求能够被发送到一个暂时不存在的资源的URI上;若是资源不存在,那就建立它,不然就修改它。

因此感受使用PUT做为Upsert的HTTP方法比较合适一些。

可是若是使用自增类主键Id的话,这种状况就不适合了。

下面咱们假设City的Id不是自增的,那么咱们能够这样修改一下Update方法:

 

因为个人例子主键是自增的,因此不适合Upsert。我就不测试了。

可是整体的思路就是这样,注意里面新增和修改返回的结果略有不一样。 

 

PATCH 局部更新资源

使用PUT最总体更新,缺点仍是很明显的,因此我更多使用的是PATCH局部更新。

HTTP PATCH请求的body部分须要使用RFC 6902 (JSOn Patch)这个标准来进行描述。

而PATCH请求的media type应该设定为 "application/json-patch+json"。

PATCH请求的body是一个操做的数组

这个例子里面有两个操做:

第一个是“replace”操做(op的值就是操做的类型),path表明着资源的属性名value表示的是更新后的值

第二个操做类型是“remove”,表示要删除资源的某个属性的值,例子里是name属性。

JSON PATCH的操做类型主要有六种:

  • 添加:{“op”: "add", "path": "/xxx", "value": "xxx"},若是该属性不存,那么就添加该属性,若是属性存在,就改变属性的值。这个对静态类型不适用。
  • 删除:{“op”: "remove", "path": "/xxx"},删除某个属性,或把它设为默认值(例如空值)。
  • 替换:{“op”: "replace", "path": "/xxx", "value": "xxx"},改变属性的值,也能够理解为先执行了删除,而后进行添加。
  • 复制:{“op”: "copy", "from": "/xxx", "path": "/yyy"},把某个属性的值赋给目标属性。
  • 移动:{“op”: "move", "from": "/xxx", "path": "/yyy"},把源属性的值赋值给目标属性,并把源属性删除或设成默认值。
  • 测试:{“op”: "test", "path": "/xxx", "value": "xxx"},测试目标属性的值和指定的值是同样的。

注意,path属性可能具备层级结构,而value属性也没必要非得是字符串。

看下代码:

传递进来的body参数须要使用JsonPatchDocument<T>这个类型,在这里我把它叫作patchDoc。首先要把EFCore的City映射成CityUpdateResource,这样这个CityUpdateResource就有了该City在数据库里最新的属性值。而后经过patchDoc.ApplyTo()这个方法把patchDoc的操做依次附加给这个CityUpdateResource,这时候全部须要更新的值都体如今CityUpdateResource里了,而该对象其它的属性值则是数据库里的最新值,也就是不须要更新的值。最后再把它的值映射给EFCore的City,进行更新就能够了。最后EFCore作的操做确定是总体更新,可是以前咱们把最新值都放在CityUpdateResource里了,因此就至关于只作了局部更新。

测试:

请求的Content-Type应该是"application/json-patch+json",可是若是之写成application/json好像也能够。

 结果:

(为了更好的测试,我又为City添加了Description属性)

下面remove的测试:

反查:

在测试一下多个操做:

结果就不看了,都是OK的。

 

PATCH用来局部更新或建立资源

 能够修改相关代码来支持局部更新或建立资源的操做:

这个我就不测试了,自增Id不适合这种操做。

 

HTTP方法适用总结

经常使用的5中HTTP方法都介绍了,下面总结一下:

GET(获取资源):

  • GET api/countries,返回200,集合数据;找不到数据返回 404。
  • GET api/countries/{id}, 返回200,单个数据;找不到返回 404.

DELETE(删除资源)

  • DELETE api/countries/{id},成功204;没找到资源 404。
  • DELETE api/countries,不多用,也是204或者404.

POST (建立资源):

  • POST api/countries, 成功返回 201 和单个数据;若是资源没有建立则返回 404
  • POST api/countries/{id},确定不会成功,返回 404或409.
  • POST api/countrycollections,成功返回 201 和集合;没建立资源则返回 404

PUT (总体更新):

  • PUT api/countries/{id}, 成功能够返回200,204;没找到资源则返回 404
  • PUT api/countries,集合操做不多见,返回 200,204或404

PATCH(局部更新):

  • PATCH api/countries/{id},200单个数据,204或者404
  • PATCH api/countries, 集合操做不多见,返回 200集合,204或404.

 

验证

为了进行输入验证(不验证输出),咱们须要作如下三方面工做:

  • 定义验证规则
  • 检查验证规则
  • 把验证错误信息发送给API的消费者

以前的文章也提到的ASP.NET Core里面定义验证规则的方式:

  • Data annotations 数据注解,就是那种在属性上面的中括号样式的属性标签
  • 如何数据注解没法知足要求,则可使用自定义的验证方式
    • 能够自定义数据注解
    • 也可让被验证类实现IValidatableObject接口
  • 也可使用像FluentApi这样的第三方验证库

检查验证规则的方式:

  • 使用 ModelState
    • 它是一个字典,包含了Model的状态以及Model所绑定的验证
    • 对于提交的每一个属性,它都包含了一个错误信息的集合
  • ModelState.IsValid(),若是出现任何一个错误,ModelState.IsValid属性就会变成false。

报告验证错误信息:

  • 返回的状态吗应该是 422 Unprocessable Entity (上文讲过,422表示请求的格式没问题,可是语义有错误,例如实体验证错误)
  • 除了状态码以外,还须要把验证错误信息在响应的body里面带回去

 

为EFCore的Model添加约束

我以前尚未为EFCore的model添加约束,这里我添加上(因为我使用的是内存数据库,因此下面的约束是不起做用的,这些约束只有在关系型数据库才起做用):

对于EFCore的实体约束和验证,我不肯意使用注解的方式(由于Model类应该只干本身的活),更喜欢使用fluent api

而后把这两个类添加到DbContext里面的OnModelCreating方法里便可:

虽然上面的代码对内存数据库没有用,可是我仍是添加上吧。

若是一个HTTP请求形成了EFCore model的验证失败,若是返回500的话,感受就不太正确。由于若是是500错误的话,就意味着是服务器出现了错误,而这其实是API消费者(客户端)提交的数据有问题,是客户端的错误。因此返回的状态码应该是 4xx 系列。

此外,目前这些验证规则是处于EFCore 的实体上的,而报告给API消费者的验证错误信息应该定义在Resource这一层面上,因此下面就为Resource model定义验证规则:

全部的验证注解能够查看官方文档:https://msdn.microsoft.com/en-us/library/system.componentmodel.dataannotations(v=vs.110).aspx

(这种方式比较简单,可是把验证和Model混合到了一块儿,因此不少人仍是不采用这种方式的)

验证规则定义完了,下面来实施规则检查。这时就须要使用ModelState了。

每当请求进入到这个方法的时候,都会验证咱们刚刚定义在Resource上的这些约束,若是其中一个约束没有达标,则ModelState的IsValid属性就会是false;此外若是传进来的属性类型和定义的不符,IsValid属性也会是false。

这里返回状态码 422 是正确的选择,可是 422 要求请求的body的语法必须是正确的,不能是null,因此前面检查是否为null的代码还须要保留。

因为ASP.NET Core并无内置的帮助方法能够返回422和验证错误信息,因此咱们先创建一个类用于返回 422 和验证错误信息,它继承于ObjectResult

其中的SerializableError定义了一个能够被串行化的容器,该容器能够以Key-Value对的形式来保存ModelState的信息。

回到CityController的POST的Action方法,只添加这部分代码便可:

下面进行测试:

能够看到验证的错误信息都按预期返回了。

再试试另一组测试:

 

下面考虑下若是据注解没法知足验证要求的状况,这时就须要写自定义的验证。

以前文章讲过,有几种方法能够写自定义验证逻辑:

  • 自定义验证属性标签(数据注解),编写一个继承于ValidationAttribute的类
  • 让Resource类实现IValidatableObject接口
  • 使用FluentValidation以及相似的第三方库
  • 直接在方法里写验证逻辑

我比较倾向于后两种方法,尤为是第三种。可是因为本文主要是讲RESTful API相关的,因此我先避免过多的使用第三方库,我暂时先采用第四种方法。

假设我要求City的name属性值不能够是“中国”:

这里要用到ModelState的AddModelError方法。

测试:

OK.

 

下面看一下PUT的验证。

大部分状况下,PUT的验证可能和POST是同样的,可是有时仍是不同的,因此分别写两个ResourceModel对应POST和PUT的优点就体现出来了。

可是这两个类的大部分代码仍是同样的,因此能够采起使用抽象父类的方法来去掉重复的代码,创建CityResource:

注意属性必定要使用virtual关键字,由于在子类里咱们可能会重写属性。

在这里我把Description的Required约束去掉了。

再看CityAddResource:

继承抽象类便可,属性和验证彻底同样。

再看CityUpdateResource:

这里,我对Description属性添加了Required约束,而其它约束和父类保持一致。

最后修改PUT的Action方法:

测试,POST:

OK。

再测试PUT,尤为是Description属性:

子类里Description的约束进行了检查。

再测试父类里Description的约束:

OK, 说明子类里Description的约束和父类里Description的约束都起做用。

在子类CityUpdateResource里,还能够这样写:

这样或许更清晰。

 

到目前为止,我使用的是数据注解的方式来为ResourceModel添加验证规则,这样作其实不是很好,没有关注点分离(Soc,Seperation of Concerns)

并且,咱们的自定义验证代码也是处处重复的写,这样也不对。

因此尽管数据注解看起来很简单,少写了一些代码,可是开发软件应该更加注重可维护性,要尽可能遵循那些设计原则,适当使用设计模式,写单元测试和E2E测试,尽管这样会形成看起来多写了一些代码,可是考虑到软件的质量以及更重要的后期维护,实际上这样作是大大的节省了成本。综上缘由,我推荐使用第三方库,FluentValidationhttps://github.com/JeremySkinner/FluentValidation

使用FluentValidation

安装FluentValidation,能够经过Nuget,Package Manager Console 或者 .net cli:

直接安装这个就能够:

而后会自动安装依赖的库:

把那些ResourceModel的数据注解验证约束都去掉,把Controller里面自定义验证的代码也去掉,而后为每个类添加一个验证器Validator:

首先是Country的,这个简单:

其中大括号里面的字符串是参数(占位符),{PropertyName}就是属性的名字若是使用了WithName()方法,那就是WithName里面设定的别名;{MaxLength}就是指设定的最大长度约束的值。有不少这种占位符,仍是须要看官方文档。

下面看看City相关的验证,这里有个继承的关系,首先是把共有的验证提取出来做为父类:

 

这里使用泛型比较好。

而后CityUpdateResource:

 

因为父子关系,父类的构造函数先执行,而后执行CityUpdateResourceValidator的构造函数。

 

最后还要为ASP.NET Core配置FluentValidation,在Startup的ConfigureServices方法里:

首先使用扩展方法AddFluentValidation();而后为每个Resource Model 配置验证器。若是你不想挨个添加配置验证器的话,可使用:

来把某个Assembly里的验证器所有添加进来,可是我仍是比较喜欢一个一个写,重构的时候有什么错误能当即发现,可是也容易忘记添加。

而后测试一下,效果和以前是同样的。

使用FluentValidation,作到了很好的分离,我我的感受很是好,虽然多写了些代码,可是更灵活,也更易于维护。

 

PATCH的验证

PATCH与POST和PUT的验证稍微有一点不一样,首先看一个例子,删除一个不存在的属性的值:

这个会致使返回500错误,这是不对的。

这时,可已使用patchDoc.ApplyTo的一个重载方法,它能够接受ModelState做为参数,因此patchDoc里面有任何验证错误都会在ModelState里面体现出来,(注意是PatchDoc的验证错误而不是CityUpdateResource)

而后从新测试:

 

我以前已经设定了CityUpdateResource的Description属性是必填的,那我再作一个PATCH测试,把该属性的值去掉(设为null):

它返回了 204, 也就是说被成功的执行了,那么确定是有些地方没有作约束检查遗漏了。

由于咱们只检查了patchDoc,而没有检查手动创建的那个CityUpdateResource(cityToPatch),因此这里可已使用TryValidateModel(xx),来手动检查cityToPatch:

测试:

此次OK了。

 

Log

在预备知识文章里,我已经介绍了Log相关的内容,因此这里就再也不重复叙述了(http://www.javashuo.com/article/p-xdbrkauk-e.html)。

看咱们以前写的捕获异常的代码,在Startup的Configure方法里:

如今的代码是为API的消费者返回了500状态码,并返回了一些错误信息。这样作咱们就把异常信息给丢掉了,可是又不该该把异常信息传递给API消费者,而咱们确实须要这个异常信息,因此咱们把异常记录到日志。

有多种方式能够获得Logger,这里我使用ILoggerFactory:

而后在Configure方法里面相应的位置建立Logger并记录日志:

整个应用的日志仍是作分类比较好,这里我使用LoggerFactory的CreateLogger方法建立了Logger,其分类是“Global Exception Logger”。

这里使用了500做为Log的EventId比较合适,毕竟是500错误。

我认为能够把Action里面返回500状态码的部分改为抛出异常。

而后我修改一下PATCH,以便能抛出一个异常:

测试:

异常被正常的抛出,在看一下控制台的Log:

Log信息也被正确的打印。

 

下面在看看如何在Controller里面记录日志,首先注入Logger:

ILogger<T>,T就是日志分类的名字,这里建议使用Controller的名字。

而后在Action里正常记录日志就能够了:

就不测试了。

 

使用Serilog

在实际应用中只把日志记录到控制台或Debug窗口是没用的,最好的办法仍是记录到文件或者数据库等。

支持ASP.NET Core的第三方Log提供商有不少,NLog,Serilog等等。这里我使用Serilog(https://github.com/serilog/serilog)。

Nuget安装:

提示安装的依赖:

而后在Program.cs里使用扩展方法UseSerilog()使用Serilog便可,我就不作其它配置了:

Serilog支持把日志写入到各类的Sinks里,能够把sink看作媒介(文件,数据库等)。

我须要写入到文件,那么就安装:

Serilog的配置信息是这样写的,能够把它放到程序比较靠前执行的地方:

这里配置的意思是:全局最低记录日志级别是Debug,可是针对以Microsoft开头的命名空间的最低级别是Information。

使用Enruch.FromLogContext()可让程序在执行上下文时动态添加或移除属性(这个须要看文档)。

按日生成记录文件,日志文件名后会带着日期,并放到./logs目录下。

这就是生成的日志文件:

注意使用了其它Log提供商以后,在它以前配置的Log提供商就不起做用了,因此控制台不输出Log的异常信息了:

因此仍是为Serilog添加一个控制台的Sink吧:

这样控制台和文件的Log均可以输出了:(注意windows下的命令行有时候会卡住,须要按一下回车才能继续)

 

此次就写到这里,下次写一些翻页和过滤的东西。

完成后的源码:https://github.com/solenovex/ASP.NET-Core-2.0-RESTful-API-Tutorial

相关文章
相关标签/搜索