本文所需的一些预备知识能够看这里: http://www.cnblogs.com/cgzl/p/9010978.html 和 http://www.cnblogs.com/cgzl/p/9019314.htmlhtml
创建Richardson成熟度2级的POST、GET、PUT、PATCH、DELETE的RESTful API请看这里:http://www.javashuo.com/article/p-sryarofa-ct.html 和 http://www.javashuo.com/article/p-nmpufjil-ee.htmlgit
本文须要的代码 (右键另存,把后缀改成zip):https://images2018.cnblogs.com/blog/986268/201806/986268-20180604151009219-514390264.jpg
github
本代码已经更新至ASP.NET Core 2.1. (从ASP.NET Core 2.0 迁移至 ASP.NET Core 2.1: https://docs.microsoft.com/en-us/aspnet/core/migration/20_21?view=aspnetcore-2.1)数据库
本文主要介绍一些常见状况的实现,包括:集合更新、翻页、排序、过滤等等。可是仍然是Richardson成熟度顶多为2级的Web API,未达到RESTful API的标准和约束。json
看这种更新集合的状况,原来数据库里中国存了4个城市(北平,上海,盛京,海参崴);而几个世纪后北平更名叫北京了,盛京改名为沈阳了,海参崴不属于中国了就删除了,威海从县成为市就算是新增,而上海保持不变。如今就是要对中国的城市进行总体性的更新操做,里面会包含:添加、删除、更新操做。看代码:后端
集合更新,我一共分了三步进行的操做:api
1. 把数据库中存在的可是传进来的数据里没有的城市删掉数组
2. 把数据库中没有的而传进来的数据里有的数据进行添加操做,其实这里只判断id为0便可并发
3. 把数据库中原有和传进来的参数里也存在的数据条目进行更新。app
而后保存便可。
先看一下原有的数据:
而后咱们执行集合的更新:
执行以后,再次查询:
集合按预期更新了。
我相信你们确定会写这段代码,或者有更简单的实现方式(请贴出来)。但这不是重点,我看到有人这样写,把上面那三步代码写在了AutoMapper的配置文件里:
首先,须要忽略Country的Cities属性的映射操做,而后把那部分代码写在AfterMap里面便可,这样在Action方法里面就简单了,可使用Automapper了:
这只是一种可选的写法而已,不必定就必须放在AutoMapper的配置文件里。
翻页能够避免一些性能问题,没必要一次性加载全部数据。因此最好默认就采用分页,并且每页的条目数量必须有限制,不能太大。
分页信息应该使用查询字符串(query stringg)传递参数。格式应该这样:
http://localhost:5000/api/country?pageIndex=12&pageSize=10
这里我喜欢使用pageIndex这个词,这也意味着页数是从0开始的;固然不少人喜欢用pageNumber等词,也就是说更喜欢页数从1开始,这个其实随意吧。
在ASP.NET Core里,我要使用Linq来动态组建一个查询的表达式(IQueryable<T>,能够建立表达式树),它是延迟执行的,直到各类条件都判断完了并组建出最终的查询表达式以后才去执行(查询数据库)。这个查询表达式只有在进行迭代的时候才会查询数据库。
触发迭代动做可使用下面的方法:
须要确保的是要在迭代发生以前,使用Skip()和Take()以及Where()。
下面我一点一点来写代码:
首先咱们须要从参数(query string参数)传进来pageIndex和pageSize,还要赋默认值,以防止API的消费者没有设置pageIndex和pageSize;因为pageSize的值是由API的消费者来定的,因此应该在后端设定一个最大值,以避免API的消费者设定一个很大的值。
因为全部的资源几乎都要使用翻页,因此咱们最好使用一个公共类来封装这些翻页相关的信息:
(我暂时把这个类放在了Core项目里)。
这个公共类很简单,能够为pageIndex和pageSize设定默认值,也设置了一个每页的最多条目数是100;这里面还有一个OrderBy属性,默认值是“Id”,由于翻页必需要先排序,但目前这个OrderBy属性还没用上。
而针对具体的资源,咱们能够再创建一个类继承于PaginationBase,这个类就是Country的参数类:
因为暂时尚未什么特别的参数,因此里面是空的。
下面我修改一下CountryRepository:
能够看到我组建了这个查询的表达式,而且直接出发了迭代动做,返回查询结果。
回到Action方法里:
我使用了这个参数类代替了以前的pageIndex和pageSize参数,由于ASP.NET Core足够智能,能够把这两个参数解析到这个类里面。
下面测试一下:
我就不进行屡次测试了,这个是好用的。
若是你是用的是关系型数据库的话,应该能够在Log的输出媒介上看到打印出的SQL语句(但我这里使用的是内存数据库,因此看不到),若是使用关系型数据库仍是看不到SQL语句的话,请配置一下:
很显然只返回当前页的数据是不知足需求的,至少还须要返回总页数,总数等信息,还有可能须要返回前一页或者后一页的连接。可是如何把这些信息连同当页的数据一块儿返回给API消费者呢?
下面的作法是能够把这些数据都返回去的:
{ “data”: [{country1}, {country2}...], “metadata”: {"prev": "/api/...", ....} }
可是这样作的话就致使了响应的body再也不符合Accept Header了(不是资源的JSON表述了),也就不是application/json了,而是一种新的media type。
因此若是返回这样的数据就违反了REST的规则了(尽管本文代码的Richardson成熟度最多也就是2级),它违反了自我描述的约束(请参考本系列的预备知识文章),API消费者不知道如何经过application/json这个设定的contety-type来解释响应数据了。
因此说翻页的元数据并非资源表述的一部分。咱们应该使用自定义的Header,例如“X-Pagination”来表述翻页元数据,这个名也是比较经常使用的。
首先,我建立一个类能够存放翻页的数据:
能够向上面这样作这个类:该类继承于List<T>,同时还包含PaginationBase做为属性,还能够判断是否有前一页和后一页。使用静态方法建立该类的实例。
这个静态方法也许会有一点点问题,这里没有使用异步方法,这样作是OK的;可是若是使用异步方法,例如source.CountAsync()和source.ToListAsync(),就会有一些问题,由于我须要修改CountryRepository的GetCountriesAsync方法的返回类型,改为上面这个类型,因此它的接口ICountryRepository也须要改;而它的接口是整个项目的核心并放在Core项目里,而整个项目的核心(合约)我我的认为应该是和具体的ORM无关的,可是这里依赖于EntityFrameworkCore了(ToListAsync())。因此我最后决定去掉这个静态方法,这样可能会致使多写一些代码;此外还添加HasPrevious和HasNext属性,判断是否有前一页和后一页:
(暂时放在Core项目里面了)。
而后修改CountryRepository:
而后在Action方法里,咱们还须要生成前一页和后一页的URI,因此这里可使用UrlHelper,须要在Startup的ConfigureServices方法里面注册:
而后回到Controller里面创建一个方法来生成URI:
在这里我还创建了一个枚举,PaginationResourceUriType。我还为PaginationBase添加了一个Clone()方法,目的是建立出一个属性值和它相同的另外一个实例,由于这里有修改pageIndex属性这个操做;也许Clone不是最好的办法,直接new可能更合适。
下面就是修改Action方法了:
经过以前的方法分别建立出两个连接,而后把翻页相关的数据组成一个匿名类,使用JSON.NET将其串行化,并放到响应的自定义Header:“X-Pagination”里面。
而body部分仍是资源的集合数据。
测试一下:
响应的body正常的返回来了,再看一下响应的Header:
能够看到自定义的X-Pagination Header了,而后我复制一下里面的NextPageLink连接,并发送该请求:
都没有问题。
这个Action目前的Richardson成熟度已经接近3级了(HATEOAS),但还不是。翻页如今是到这,下面要进行过滤并翻页。
过滤的意思就是对集合资源附加一些条件而后筛选出结果,它的URI是下面的形式:
http://localhost:5000/api/countries?englishName=China
因此须要在查询字符串里写上属性的名字和属性的值来表示要按这个属性的值来进行过滤,固然也能够写多个过滤的条件。
过滤的条件是应用于ResourceModel(或叫作Dto,ViewModel),例如CountryResource,而不该用于其它级别的Model,由于API消费者只知道ResourceModel,它不知道内部实现的细节,也就是不知道EntityModel的样子。
而搜索呢,是经过一个搜索关键字来模糊的筛选集合资源,可能会有多个属性针对这个关键字进行模糊筛选。
搜索的URI大体是下面的形式:
http://localhost/api/countries?searchTerm=hin
上面这个URI能够理解为针对Countries资源,凡是字符串类型的属性,它的值包含hin的都符合条件,就返回符合这个条件的结果。
首先看一下过滤的实现。在Countries的GET Action方法里,我使用CountryResourceParameters类做为参数,因此要增长针对某个属性的过滤条件,只需扩展这个类便可,而增长的属性名要和ResourceModel里面的属性名一致:
而后是修改CountryRepository里面的方法:
首先要在执行分页动做以前附加过滤条件,query的类型必须是IQueryable<Country>才能够动态组建查询表达式,因此使用了AsQueryable()方法;而后分别判断两个条件并附加条件(注意大小写问题和两头空格的问题),最后再执行分页查询。
因为添加了参数,因此CreateUri的方法也须要改:
这个方法参数变成了CountryResourceParameters,并且Clone方法克隆出来的也是CountryResourceParameters类:
下面测试:
没有问题的,可是还要看看Header:
针对这个结果是OK的。
下面我作一些数据,使其拥有一样的EnglishName,而后测试:
OK,再看看Header:
使用NextLink再次发送请求, 结果是OK的,我就不贴图了。
可是你应该注意到,X-Pagination的属性名不符合camelCase命名规范,因此须要在转化成JSON的时候添加一些配置:
而后再测试一下:
属性的命名符合camelcase规范了,可是previousLink和nextLink里面的查询字符串的大小写依然不正确,因此我干脆去掉了Clone()方法,而后在CreateCountryUri的方法里直接new出来新连接的参数:
测试:
如今命名终于符合规范了。
以前作的翻页都须要排序,暂时都是按照Id进行排序的。而实际上API消费者可能让资源按照资源的某个属性或多个属性进行正向或反向的排序。
咱们先从最简单的例子开始,只考虑只按照某一个属性(针对的是资源的属性,例如CountryResource的EnglishName)进行排序,针对这个例子,我先使用比较笨的方法。
首先我假定,参数类里面的OrderBy属性若是以" desc" 结尾,例如:“EnglishName desc”,那么就是按照EnglishName倒序排列,而“EnglishName”就是正序排列。
只需在CountryRepository里面修改代码便可:
嗯,很笨重的代码。
先测试一下:
至少功能是OK的,再看一下倒序:
也OK,因此虽然代码很笨重,可是针对这种简单的状况是能够应付的。
下面咱们对它进行第一次优化。像上面这样挨个属性的判断实在是太费劲了,因此咱们来分析一下,OrderBy的值是字符串,而OrderBy()方法里面的lambda表达式的类型是Expression,具体的类型是Expression<Func<Country, object>>。这里简单讲一下,万一您不知道lambda表达式的话能够看一下。lambda表达式就是匿名的函数,它的类型是Func(能够赋值给Func类型的变量):
同时咱们也能够把这个lambda表达式赋值给Expression:
而OrderBy()这个Linq方法接收的参数类型就是Expression<Func<Country, object>>。
使用Expression,咱们能够构建Expression Tree;使用Expression Tree,能够表示一些逻辑。而在运行时,Linq的提供商将会解析这个Expression Tree,并把这些逻辑转化为SQL语句:
再看上面的排序条件判断,咱们能够把OrderBy的字符串和Expression映射起来,就像Key-Value 键值对那样,这样作也许就会是代码稍微好看一些。因此你确定会想到Dictionary<K, V>。
因此修改后的代码以下:
我相信你能看懂,我就不解释了,下面测试:
总之是好用的,我就不贴其余测试结果的图片了。
应该把上面这段代码提取出来封装成一个方法函数并泛型化,可是我暂时先不这样作。
通过第一次优化,使用Dictionary,代码简洁了许多,可是期间仍是有手动把属性名字符串转化为Expression的动做。之因此这么写是由于OrderBy仅支持Expression的参数类型,若是支持字符串,那就完美了。
幸亏有一个微软的库支持这种操做,它叫作System.Linq.Dynamic.Core(其做者是红衣教主啊):
我把它安装在了Infrastructure项目里供Repository使用。
再次修改排序那部分的代码:
注意这里OrderBy的命名空间是:System.Linq.Dynamic.Core。
通过第二次优化,代码已经很简洁了,可是还有不少待完善的地方,例如:
第三次优化,要解决Model属性映射引发的问题。
也就是说要从ResourceModel的一个属性映射到Entity Model的一个或者多个属性上,并且它们之间的排列顺序多是不一样的,举一个极端的例子:
假设ResourceModel 有个属性叫作 Rank(排名) ,它所映射Entity Model的两个属性Result(成绩)和Weight(体重);假设这是举重比赛的Model,排名结果(Rank)是按照成绩(Result)从高到低排序的,可是若是多名选手的成绩相同,则体重轻的排名靠前。
也就是Rank asc -> Result desc, Weight asc。
用程序来讲就是,一个字符串“Rank asc”要映射成一个集合,而集合元素的类型有两个属性:Entity Model的属性名和排序的方向。
因此先把集合里这种元素的类创建出来:
这里方向我是用的Revert这个单词,表示其方向是否与Resource Model的属性方向相反便可。
而后在作针对CountryResource的整套映射,不过首先我考虑创建一个抽象父类,里面可能有些公用的东西:
因为Id这个属性多是每一个相关的Model共有的,因此在这个父类里,我添加了Id属性的映射,Id是一对一的映射,排序方向相同。
而后我针对CountryResource,写一个派生于PropertyMapping的子类:
注意红框很重要,比较key的时候忽略大小写。
到这里,Resource和Entity Model之间映射的部分差很少作完了,接下来要考虑整个排序的问题,作这样一个扩展方法:
它应用于IQueryable,并把orderBy字符串和属性映射表传进来。
通过一些初步检验以后,把orderBy按“,”分解成字段属性的数组。而后去掉两边可能存在的空格,判断是不是倒序,提取出属性的名称。若是在映射表里面找不到该名称或者该名称对应的值是空,那就抛出异常。
而后先循环字段数组,而后内层循环该字段映射的属性集合。
最后经过DynamicLinq便可组建出所需的排序表达式。
使用DynamicLinq的OrderBy时要注意,排序条件必须反向附加,不信能够试试。
随后咱们修改一下Repository:
就剩下一句话了,很简洁了。可是这里须要new一个CountryPropertyMapping类,这样作对单元测试就不友好了,也许把它放在一个容器里取出来用更合适?
那么就创建一个容器:
该容器的Register和Resolve分别用来注册和提取映射表。
下面还有个检查映射是否存在的方法,fields是一个或者多个字段属性组成的字符串,其格式如“EnglishName,ChineseName”;它检查是否能在映射配置表(MappingDictionary)找到相应的Key,若是找不到就验证失败。
这个容器在整个应用范围内也是个容器,因此须要在Startup里面注册,因为它的代码可能比较多(由于自己它也是个容器,还有不少注册内容用的代码),因此我单独写了个扩展方法:
该方法能够在Startup里面调用,从而注册到ASP.NET Core的服务容器里:
而后再次修改CountryRepository:
先注入了该容器服务,而后从该容器中按照映射两端的Model类型取出须要的映射表:
测试:
看起来是OK的,那咱们针对排序,暂时先优化到这里。
还须要考虑到若是OrderBy里面的字段在映射表里面不存在的状况,因此我使用这个方法来进行判断:
我把这个方法放在了PropertyMappingContainer里,由于PropertyMappingContainer自己实际上就是一个服务,放在里仍是比较合适的。
这里须要注意的是fileds里面的字段多是这种形式的“EnglishName desc”,因此须要把空格和desc部分去掉。
随后在Action方法里调用便可:
测试:
应该是没问题的,我就很少测试了,之后要实行单元测试的。
若是某个资源的属性比较多,那么客户端的API消费者可能只须要一部分属性,这时就应该进行数据塑形,并且这样作有可能会提高性能。
数据塑形要考虑两种状况,集合资源和单个资源。
先考虑集合资源,首先我作一个扩展方法,把IEnumerable<T>能够转化为IEnumerable<dynamic>,这里要用到dynamic(ExpandoObject):
因为反射比较消耗资源,因此在这里,我一次性把须要的属性弄成PropertyInfo放到了一个集合里。若是fields是空的,说明须要全部属性,就把全部public和实例的property都放到集合里,不然,就把须要的属性放进去便可。
而后循环数据源,使用反射经过PropertyInfo获取该属性的值,最后组成一个ExpandoObject,再把这个ExpandoObject放到结果集合里面便可。
接下来修改参数类,由于这是个通用的东西,那就是为PaginationBase添加一个Fields属性吧:
最后修改Action方法:
测试:
好用的。可是返回的数据并非camelcase的,这是由于JSON.net串行化的ContractResolver并不适用于Dictionary。下面来处理这个问题。
打开Startup,在services.AddMvc()后边添加:
这句话就是配置了JSON转化的ContractResolver。
在测试一下:
如今Ok了。
但若是API消费者在Fields里面提供了不存在的属性,那么就应该返回Bad Request。
原理上我也许可使用ProperyMappingContainer里面的验证方法,可是数据塑形并不使用映射表。并且目的不一样,一个是排序一个是数据塑形,因此由于关注分离吧(SoC)。
咱们要作的就是给定一个Fields和一个类型,须要判断Fields里面包含的字段属性在这个类型里面都存在,因此仍是作一个Service比较好,能够注入使用。
看代码:
这个类比较简单很少讲了,别忘了在Startup里面注册。
而后在Controller里面注入并使用,别忘了还须要修改CreateCountryUri方法:
测试:
OK.
这个跟集合的原理差很少,先创建一个扩展方法:
再修改Action便可:
测试:
是好用的,我就很少测试了。
针对数据塑形须要注意的是,尽可能把Id带上,不然可能没法获取相关的连接了。
今天先写到这里,还有不少更深刻一点的功能没有作,我就不作了。
到目前为止,这些Web API仍然称不上是RESTful的API,成熟度不够高,有些约束也没达到。下一篇文章会把升级这些API以便支持HATEOAS。
代码在这:https://github.com/solenovex/ASP.NET-Core-2.0-RESTful-API-Tutorial
项目有一些文件的拜访目录可能不对,暂时先不处理。