-
返回的状态吗应该是 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方法。
下面看一下PUT的验证。
大部分情况下,PUT的验证可能和POST是一样的,但是有时还是不一样的,所以分别写两个ResourceModel对应POST和PUT的优势就体现出来了。
但是这两个类的大部分代码还是一样的,所以可以采取使用抽象父类的方法来去掉重复的代码,建立CityResource:
注意属性一定要使用virtual关键字,因为在子类里我们可能会重写属性。
在这里我把Description的Required约束去掉了。
再看CityAddResource:
继承抽象类即可,属性和验证完全一样。
再看CityUpdateResource:
这里,我对Description属性添加了Required约束,而其它约束和父类保持一致。
最后修改PUT的Action方法:
测试,POST:
再测试PUT,尤其是Description属性:
子类里Description的约束进行了检查。
再测试父类里Description的约束:
OK, 说明子类里Description的约束和父类里Description的约束都起作用。
在子类CityUpdateResource里,还可以这样写:
这样或许更清晰。
到目前为止,我使用的是数据注解的方式来为ResourceModel添加验证规则,这样做其实不是很好,没有关注点分离(Soc,Seperation of Concerns)。
而且,我们的自定义验证代码也是到处重复的写,这样也不对。
所以尽管数据注解看起来很简单,少写了一些代码,但是开发软件应该更加注重可维护性,要尽量遵循那些设计原则,适当使用设计模式,写单元测试和E2E测试,尽管这样会造成看起来多写了一些代码,但是考虑到软件的质量以及更重要的后期维护,实际上这样做是大大的节省了成本。综上原因,我推荐使用第三方库,FluentValidation:https://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相关的内容,所以这里就不再重复叙述了
看我们之前写的捕获异常的代码,在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