用ASP.NET Core 2.1 建立规范的 REST API -- 翻页/排序/过滤等
本文所需的一些預(yù)備知識(shí)可以看這里:??用ASP.NET Core 2.0 建立規(guī)范的 REST API -- 預(yù)備知識(shí)?和??用ASP.NET Core 2.0 建立規(guī)范的 REST API -- 預(yù)備知識(shí) (2) + 準(zhǔn)備項(xiàng)目
建立Richardson成熟度2級(jí)的POST、GET、PUT、PATCH、DELETE的RESTful API請(qǐng)看這里:?用ASP.NET Core 2.0 建立規(guī)范的 REST API -- DELETE, UPDATE, PATCH 和 Log
本文需要的代碼 (右鍵另存,把后綴改為zip):https://images2018.cnblogs.com/blog/986268/201806/986268-20180604151009219-514390264.jpg
本代碼已經(jīng)更新至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)
?
本文主要介紹一些常見(jiàn)情況的實(shí)現(xiàn),包括:集合更新、翻頁(yè)、排序、過(guò)濾等等。但是仍然是Richardson成熟度頂多為2級(jí)的Web API,未達(dá)到RESTful API的標(biāo)準(zhǔn)和約束。
?
集合的更新操作
?
看這種更新集合的情況,原來(lái)數(shù)據(jù)庫(kù)里中國(guó)存了4個(gè)城市(北平,上海,盛京,海參崴);而幾個(gè)世紀(jì)后北平改名叫北京了,盛京改名為沈陽(yáng)了,海參崴不屬于中國(guó)了就刪除了,威海從縣成為市就算是新增,而上海保持不變。現(xiàn)在就是要對(duì)中國(guó)的城市進(jìn)行整體性的更新操作,里面會(huì)包含:添加、刪除、更新操作。看代碼:
集合更新,我一共分了三步進(jìn)行的操作:
1. 把數(shù)據(jù)庫(kù)中存在的但是傳進(jìn)來(lái)的數(shù)據(jù)里沒(méi)有的城市刪掉
2. 把數(shù)據(jù)庫(kù)中沒(méi)有的而傳進(jìn)來(lái)的數(shù)據(jù)里有的數(shù)據(jù)進(jìn)行添加操作,其實(shí)這里只判斷id為0即可
3. 把數(shù)據(jù)庫(kù)中原有和傳進(jìn)來(lái)的參數(shù)里也存在的數(shù)據(jù)條目進(jìn)行更新。
然后保存即可。
先看一下原有的數(shù)據(jù):
然后我們執(zhí)行集合的更新:
執(zhí)行之后,再次查詢:
集合按預(yù)期更新了。
我相信大家肯定會(huì)寫(xiě)這段代碼,或者有更簡(jiǎn)單的實(shí)現(xiàn)方式(請(qǐng)貼出來(lái))。但這不是重點(diǎn),我看到有人這樣寫(xiě),把上面那三步代碼寫(xiě)在了AutoMapper的配置文件里:
首先,需要忽略Country的Cities屬性的映射操作,然后把那部分代碼寫(xiě)在AfterMap里面即可,這樣在Action方法里面就簡(jiǎn)單了,可以使用Automapper了:
這只是一種可選的寫(xiě)法而已,不一定就必須放在AutoMapper的配置文件里。
?
翻頁(yè)
翻頁(yè)可以避免一些性能問(wèn)題,不必一次性加載所有數(shù)據(jù)。所以最好默認(rèn)就采用分頁(yè),而且每頁(yè)的條目數(shù)量必須有限制,不能太大。
分頁(yè)信息應(yīng)該使用查詢字符串(query stringg)傳遞參數(shù)。格式應(yīng)該這樣:
http://localhost:5000/api/country?pageIndex=12&pageSize=10
這里我喜歡使用pageIndex這個(gè)詞,這也意味著頁(yè)數(shù)是從0開(kāi)始的;當(dāng)然很多人喜歡用pageNumber等詞,也就是說(shuō)更喜歡頁(yè)數(shù)從1開(kāi)始,這個(gè)其實(shí)隨意吧。
在ASP.NET Core里,我要使用Linq來(lái)動(dòng)態(tài)組建一個(gè)查詢的表達(dá)式(IQueryable<T>,可以創(chuàng)建表達(dá)式樹(shù)),它是延遲執(zhí)行的,直到各種條件都判斷完了并組建出最終的查詢表達(dá)式之后才去執(zhí)行(查詢數(shù)據(jù)庫(kù))。這個(gè)查詢表達(dá)式只有在進(jìn)行迭代的時(shí)候才會(huì)查詢數(shù)據(jù)庫(kù)。
觸發(fā)迭代動(dòng)作可以使用下面的方法:
foreach?循環(huán)
ToList(), ToArray(), ToDictionary()?以及相應(yīng)的異步版本(ToXxxxAsync())
單項(xiàng)查詢,例如 Average(), Count(), First(), FirstOrDefault(), SingleOrDefault()等等,以及相應(yīng)的異步版本。
需要確保的是要在迭代發(fā)生之前,使用Skip()和Take()以及Where()。
下面我一點(diǎn)一點(diǎn)來(lái)寫(xiě)代碼:
首先我們需要從參數(shù)(query string參數(shù))傳進(jìn)來(lái)pageIndex和pageSize,還要賦默認(rèn)值,以防止API的消費(fèi)者沒(méi)有設(shè)置pageIndex和pageSize;由于pageSize的值是由API的消費(fèi)者來(lái)定的,所以應(yīng)該在后端設(shè)定一個(gè)最大值,以免API的消費(fèi)者設(shè)定一個(gè)很大的值。
由于所有的資源幾乎都要使用翻頁(yè),所以我們最好使用一個(gè)公共類來(lái)封裝這些翻頁(yè)相關(guān)的信息:
(我暫時(shí)把這個(gè)類放在了Core項(xiàng)目里)。
這個(gè)公共類很簡(jiǎn)單,可以為pageIndex和pageSize設(shè)定默認(rèn)值,也設(shè)置了一個(gè)每頁(yè)的最多條目數(shù)是100;這里面還有一個(gè)OrderBy屬性,默認(rèn)值是“Id”,因?yàn)榉?yè)必須要先排序,但目前這個(gè)OrderBy屬性還沒(méi)用上。
而針對(duì)具體的資源,我們可以再建立一個(gè)類繼承于PaginationBase,這個(gè)類就是Country的參數(shù)類:
由于暫時(shí)還沒(méi)有什么特別的參數(shù),所以里面是空的。
下面我修改一下CountryRepository:
可以看到我組建了這個(gè)查詢的表達(dá)式,并且直接出發(fā)了迭代動(dòng)作,返回查詢結(jié)果。
回到Action方法里:
我使用了這個(gè)參數(shù)類代替了之前的pageIndex和pageSize參數(shù),因?yàn)锳SP.NET Core足夠智能,可以把這兩個(gè)參數(shù)解析到這個(gè)類里面。
下面測(cè)試一下:
我就不進(jìn)行多次測(cè)試了,這個(gè)是好用的。
如果你是用的是關(guān)系型數(shù)據(jù)庫(kù)的話,應(yīng)該可以在Log的輸出媒介上看到打印出的SQL語(yǔ)句(但我這里使用的是內(nèi)存數(shù)據(jù)庫(kù),所以看不到),如果使用關(guān)系型數(shù)據(jù)庫(kù)還是看不到SQL語(yǔ)句的話,請(qǐng)配置一下:
返回翻頁(yè)的元數(shù)據(jù)
很顯然只返回當(dāng)前頁(yè)的數(shù)據(jù)是不滿足需求的,至少還需要返回總頁(yè)數(shù),總數(shù)等信息,還有可能需要返回前一頁(yè)或者后一頁(yè)的鏈接。但是如何把這些信息連同當(dāng)頁(yè)的數(shù)據(jù)一起返回給API消費(fèi)者呢?
下面的做法是可以把這些數(shù)據(jù)都返回去的:
{“data”: [{country1}, {country2}...],“metadata”: {"prev": "/api/...", ....} ? ? }但是這樣做的話就導(dǎo)致了響應(yīng)的body不再符合Accept Header了(不是資源的JSON表述了),也就不是application/json了,而是一種新的media type。
所以如果返回這樣的數(shù)據(jù)就違反了REST的規(guī)則了(盡管本文代碼的Richardson成熟度最多也就是2級(jí)),它違反了自我描述的約束(請(qǐng)參考本系列的預(yù)備知識(shí)文章),API消費(fèi)者不知道如何通過(guò)application/json這個(gè)設(shè)定的contety-type來(lái)解釋響應(yīng)數(shù)據(jù)了。
所以說(shuō)翻頁(yè)的元數(shù)據(jù)并不是資源表述的一部分。我們應(yīng)該使用自定義的Header,例如“X-Pagination”來(lái)表述翻頁(yè)元數(shù)據(jù),這個(gè)名也是比較常用的。
首先,我創(chuàng)建一個(gè)類可以存放翻頁(yè)的數(shù)據(jù):
可以向上面這樣做這個(gè)類:該類繼承于List<T>,同時(shí)還包含PaginationBase作為屬性,還可以判斷是否有前一頁(yè)和后一頁(yè)。使用靜態(tài)方法創(chuàng)建該類的實(shí)例。
這個(gè)靜態(tài)方法也許會(huì)有一點(diǎn)點(diǎn)問(wèn)題,這里沒(méi)有使用異步方法,這樣做是OK的;但是如果使用異步方法,例如source.CountAsync()和source.ToListAsync(),就會(huì)有一些問(wèn)題,因?yàn)槲倚枰薷腃ountryRepository的GetCountriesAsync方法的返回類型,改成上面這個(gè)類型,所以它的接口ICountryRepository也需要改;而它的接口是整個(gè)項(xiàng)目的核心并放在Core項(xiàng)目里,而整個(gè)項(xiàng)目的核心(合約)我個(gè)人認(rèn)為應(yīng)該是和具體的ORM無(wú)關(guān)的,但是這里依賴于EntityFrameworkCore了(ToListAsync())。所以我最后決定去掉這個(gè)靜態(tài)方法,這樣可能會(huì)導(dǎo)致多寫(xiě)一些代碼;此外還添加HasPrevious和HasNext屬性,判斷是否有前一頁(yè)和后一頁(yè):
(暫時(shí)放在Core項(xiàng)目里面了)。
然后修改CountryRepository:
然后在Action方法里,我們還需要生成前一頁(yè)和后一頁(yè)的URI,所以這里可以使用UrlHelper,需要在Startup的ConfigureServices方法里面注冊(cè):
然后回到Controller里面建立一個(gè)方法來(lái)生成URI:
在這里我還建立了一個(gè)枚舉,PaginationResourceUriType。我還為PaginationBase添加了一個(gè)Clone()方法,目的是創(chuàng)建出一個(gè)屬性值和它相同的另一個(gè)實(shí)例,因?yàn)檫@里有修改pageIndex屬性這個(gè)操作;也許Clone不是最好的辦法,直接new可能更合適。
下面就是修改Action方法了:
通過(guò)之前的方法分別創(chuàng)建出兩個(gè)鏈接,然后把翻頁(yè)相關(guān)的數(shù)據(jù)組成一個(gè)匿名類,使用JSON.NET將其串行化,并放到響應(yīng)的自定義Header:“X-Pagination”里面。
而body部分還是資源的集合數(shù)據(jù)。
測(cè)試一下:
響應(yīng)的body正常的返回來(lái)了,再看一下響應(yīng)的Header:
可以看到自定義的X-Pagination Header了,然后我復(fù)制一下里面的NextPageLink鏈接,并發(fā)送該請(qǐng)求:
都沒(méi)有問(wèn)題。
這個(gè)Action目前的Richardson成熟度已經(jīng)接近3級(jí)了(HATEOAS),但還不是。翻頁(yè)現(xiàn)在是到這,下面要進(jìn)行過(guò)濾并翻頁(yè)。
?
過(guò)濾和搜索
過(guò)濾的意思就是對(duì)集合資源附加一些條件然后篩選出結(jié)果,它的URI是下面的形式:
http://localhost:5000/api/countries?englishName=China所以需要在查詢字符串里寫(xiě)上屬性的名字和屬性的值來(lái)表示要按這個(gè)屬性的值來(lái)進(jìn)行過(guò)濾,當(dāng)然也可以寫(xiě)多個(gè)過(guò)濾的條件。
過(guò)濾的條件是應(yīng)用于ResourceModel(或叫做Dto,ViewModel),例如CountryResource,而不應(yīng)用于其它級(jí)別的Model,因?yàn)锳PI消費(fèi)者只知道ResourceModel,它不知道內(nèi)部實(shí)現(xiàn)的細(xì)節(jié),也就是不知道EntityModel的樣子。
?
而搜索呢,是通過(guò)一個(gè)搜索關(guān)鍵字來(lái)模糊的篩選集合資源,可能會(huì)有多個(gè)屬性針對(duì)這個(gè)關(guān)鍵字進(jìn)行模糊篩選。
搜索的URI大致是下面的形式:
http://localhost/api/countries?searchTerm=hin?
上面這個(gè)URI可以理解為針對(duì)Countries資源,凡是字符串類型的屬性,它的值包含hin的都符合條件,就返回符合這個(gè)條件的結(jié)果。
首先看一下過(guò)濾的實(shí)現(xiàn)。在Countries的GET Action方法里,我使用CountryResourceParameters類作為參數(shù),所以要增加針對(duì)某個(gè)屬性的過(guò)濾條件,只需擴(kuò)展這個(gè)類即可,而增加的屬性名要和ResourceModel里面的屬性名一致:
然后是修改CountryRepository里面的方法:
首先要在執(zhí)行分頁(yè)動(dòng)作之前附加過(guò)濾條件,query的類型必須是IQueryable<Country>才可以動(dòng)態(tài)組建查詢表達(dá)式,所以使用了AsQueryable()方法;然后分別判斷兩個(gè)條件并附加條件(注意大小寫(xiě)問(wèn)題和兩頭空格的問(wèn)題),最后再執(zhí)行分頁(yè)查詢。
由于添加了參數(shù),所以CreateUri的方法也需要改:
這個(gè)方法參數(shù)變成了CountryResourceParameters,而且Clone方法克隆出來(lái)的也是CountryResourceParameters類:
下面測(cè)試:
沒(méi)有問(wèn)題的,但是還要看看Header:
針對(duì)這個(gè)結(jié)果是OK的。
下面我做一些數(shù)據(jù),使其擁有同樣的EnglishName,然后測(cè)試:
?
?OK,再看看Header:
使用NextLink再次發(fā)送請(qǐng)求, 結(jié)果是OK的,我就不貼圖了。
但是你應(yīng)該注意到,X-Pagination的屬性名不符合camelCase命名規(guī)范,所以需要在轉(zhuǎn)化成JSON的時(shí)候添加一些配置:
然后再測(cè)試一下:
屬性的命名符合camelcase規(guī)范了,但是previousLink和nextLink里面的查詢字符串的大小寫(xiě)依然不正確,所以我干脆去掉了Clone()方法,然后在CreateCountryUri的方法里直接new出來(lái)新鏈接的參數(shù):
測(cè)試:
現(xiàn)在命名終于符合規(guī)范了。
?
排序
之前做的翻頁(yè)都需要排序,暫時(shí)都是按照Id進(jìn)行排序的。而實(shí)際上API消費(fèi)者可能讓資源按照資源的某個(gè)屬性或多個(gè)屬性進(jìn)行正向或反向的排序。
我們先從最簡(jiǎn)單的例子開(kāi)始,只考慮只按照某一個(gè)屬性(針對(duì)的是資源的屬性,例如CountryResource的EnglishName)進(jìn)行排序,針對(duì)這個(gè)例子,我先使用比較笨的方法。
首先我假定,參數(shù)類里面的OrderBy屬性如果以" desc" 結(jié)尾,例如:“EnglishName desc”,那么就是按照EnglishName倒序排列,而“EnglishName”就是正序排列。
只需在CountryRepository里面修改代碼即可:
?
嗯,很笨重的代碼。
先測(cè)試一下:
至少功能是OK的,再看一下倒序:
也OK,所以雖然代碼很笨重,但是針對(duì)這種簡(jiǎn)單的情況是可以應(yīng)付的。
下面我們對(duì)它進(jìn)行第一次優(yōu)化。像上面這樣挨個(gè)屬性的判斷實(shí)在是太費(fèi)勁了,所以我們來(lái)分析一下,OrderBy的值是字符串,而OrderBy()方法里面的lambda表達(dá)式的類型是Expression,具體的類型是Expression<Func<Country, object>>。這里簡(jiǎn)單講一下,萬(wàn)一您不知道lambda表達(dá)式的話可以看一下。lambda表達(dá)式就是匿名的函數(shù),它的類型是Func(可以賦值給Func類型的變量):
同時(shí)我們也可以把這個(gè)lambda表達(dá)式賦值給Expression:
而OrderBy()這個(gè)Linq方法接收的參數(shù)類型就是Expression<Func<Country, object>>。
使用Expression,我們可以構(gòu)建Expression Tree;使用Expression Tree,可以表示一些邏輯。而在運(yùn)行時(shí),Linq的提供商將會(huì)解析這個(gè)Expression Tree,并把這些邏輯轉(zhuǎn)化為SQL語(yǔ)句:
再看上面的排序條件判斷,我們可以把OrderBy的字符串和Expression映射起來(lái),就像Key-Value 鍵值對(duì)那樣,這樣做也許就會(huì)是代碼稍微好看一些。所以你肯定會(huì)想到Dictionary<K, V>。
所以修改后的代碼如下:
我相信你能看懂,我就不解釋了,下面測(cè)試:
總之是好用的,我就不貼其他測(cè)試結(jié)果的圖片了。
應(yīng)該把上面這段代碼提取出來(lái)封裝成一個(gè)方法函數(shù)并泛型化,但是我暫時(shí)先不這樣做。
?
經(jīng)過(guò)第一次優(yōu)化,使用Dictionary,代碼簡(jiǎn)潔了許多,但是期間還是有手動(dòng)把屬性名字符串轉(zhuǎn)化為Expression的動(dòng)作。之所以這么寫(xiě)是因?yàn)镺rderBy僅支持Expression的參數(shù)類型,如果支持字符串,那就完美了。
幸好有一個(gè)微軟的庫(kù)支持這種操作,它叫做System.Linq.Dynamic.Core(其作者是紅衣教主啊):
我把它安裝在了Infrastructure項(xiàng)目里供Repository使用。
再次修改排序那部分的代碼:
注意這里OrderBy的命名空間是:System.Linq.Dynamic.Core。
經(jīng)過(guò)第二次優(yōu)化,代碼已經(jīng)很簡(jiǎn)潔了,但是還有很多待完善的地方,例如:
Resource Model的一個(gè)屬性可能會(huì)映射到Entity Model的多個(gè)屬性上:Name 屬性通常會(huì)映射成EntityModel的 FirstName 和 LastName屬性
Resource Model上的正序可能在Entity Model上就是倒序的:Age 升序,而Entity Model的BirthDate就是降序
需要支持多屬性的排序:EnglishName desc, Id, ChineseName。
復(fù)用
?
第三次優(yōu)化,要解決Model屬性映射引起的問(wèn)題。
也就是說(shuō)要從ResourceModel的一個(gè)屬性映射到Entity Model的一個(gè)或者多個(gè)屬性上,而且它們之間的排列順序可能是不同的,舉一個(gè)極端的例子:
假設(shè)ResourceModel 有個(gè)屬性叫做 Rank(排名) ,它所映射Entity Model的兩個(gè)屬性Result(成績(jī))和Weight(體重);假設(shè)這是舉重比賽的Model,排名結(jié)果(Rank)是按照成績(jī)(Result)從高到低排序的,但是如果多名選手的成績(jī)相同,則體重輕的排名靠前。
也就是Rank asc -> Result desc, Weight asc。
用程序來(lái)說(shuō)就是,一個(gè)字符串“Rank asc”要映射成一個(gè)集合,而集合元素的類型有兩個(gè)屬性:Entity Model的屬性名和排序的方向。
所以先把集合里這種元素的類建立出來(lái):
這里方向我是用的Revert這個(gè)單詞,表示其方向是否與Resource Model的屬性方向相反即可。
然后在做針對(duì)CountryResource的整套映射,不過(guò)首先我考慮建立一個(gè)抽象父類,里面可能有些公用的東西:
由于Id這個(gè)屬性可能是每個(gè)相關(guān)的Model共有的,所以在這個(gè)父類里,我添加了Id屬性的映射,Id是一對(duì)一的映射,排序方向相同。
然后我針對(duì)CountryResource,寫(xiě)一個(gè)派生于PropertyMapping的子類:
注意紅框很重要,比較key的時(shí)候忽略大小寫(xiě)。
到這里,Resource和Entity Model之間映射的部分差不多做完了,接下來(lái)要考慮整個(gè)排序的問(wèn)題,做這樣一個(gè)擴(kuò)展方法:
它應(yīng)用于IQueryable,并把orderBy字符串和屬性映射表傳進(jìn)來(lái)。
經(jīng)過(guò)一些初步檢驗(yàn)之后,把orderBy按“,”分解成字段屬性的數(shù)組。然后去掉兩邊可能存在的空格,判斷是否是倒序,提取出屬性的名稱。如果在映射表里面找不到該名稱或者該名稱對(duì)應(yīng)的值是空,那就拋出異常。
然后先循環(huán)字段數(shù)組,然后內(nèi)層循環(huán)該字段映射的屬性集合。
最后通過(guò)DynamicLinq即可組建出所需的排序表達(dá)式。
使用DynamicLinq的OrderBy時(shí)要注意,排序條件必須反向附加,不信可以試試。
隨后我們修改一下Repository:
就剩下一句話了,很簡(jiǎn)潔了。但是這里需要new一個(gè)CountryPropertyMapping類,這樣做對(duì)單元測(cè)試就不友好了,也許把它放在一個(gè)容器里取出來(lái)用更合適?
那么就建立一個(gè)容器:
該容器的Register和Resolve分別用來(lái)注冊(cè)和提取映射表。
下面還有個(gè)檢查映射是否存在的方法,fields是一個(gè)或者多個(gè)字段屬性組成的字符串,其格式如“EnglishName,ChineseName”;它檢查是否能在映射配置表(MappingDictionary)找到相應(yīng)的Key,如果找不到就驗(yàn)證失敗。
這個(gè)容器在整個(gè)應(yīng)用范圍內(nèi)也是個(gè)容器,所以需要在Startup里面注冊(cè),由于它的代碼可能比較多(因?yàn)楸旧硭彩莻€(gè)容器,還有很多注冊(cè)內(nèi)容用的代碼),所以我單獨(dú)寫(xiě)了個(gè)擴(kuò)展方法:
該方法可以在Startup里面調(diào)用,從而注冊(cè)到ASP.NET Core的服務(wù)容器里:
然后再次修改CountryRepository:
先注入了該容器服務(wù),然后從該容器中按照映射兩端的Model類型取出需要的映射表:
?測(cè)試:
看起來(lái)是OK的,那我們針對(duì)排序,暫時(shí)先優(yōu)化到這里。
?
排序的異常
還需要考慮到如果OrderBy里面的字段在映射表里面不存在的情況,所以我使用這個(gè)方法來(lái)進(jìn)行判斷:
我把這個(gè)方法放在了PropertyMappingContainer里,因?yàn)镻ropertyMappingContainer本身實(shí)際上就是一個(gè)服務(wù),放在里還是比較合適的。
這里需要注意的是fileds里面的字段可能是這種形式的“EnglishName desc”,所以需要把空格和desc部分去掉。
隨后在Action方法里調(diào)用即可:
測(cè)試:
應(yīng)該是沒(méi)問(wèn)題的,我就不多測(cè)試了,以后要實(shí)行單元測(cè)試的。
?
資源塑形
如果某個(gè)資源的屬性比較多,那么客戶端的API消費(fèi)者可能只需要一部分屬性,這時(shí)就應(yīng)該進(jìn)行數(shù)據(jù)塑形,而且這樣做有可能會(huì)提升性能。
數(shù)據(jù)塑形要考慮兩種情況,集合資源和單個(gè)資源。
集合資源塑形
先考慮集合資源,首先我做一個(gè)擴(kuò)展方法,把IEnumerable<T>可以轉(zhuǎn)化為IEnumerable<dynamic>,這里要用到dynamic(ExpandoObject):
由于反射比較消耗資源,所以在這里,我一次性把需要的屬性弄成PropertyInfo放到了一個(gè)集合里。如果fields是空的,說(shuō)明需要所有屬性,就把所有public和實(shí)例的property都放到集合里,否則,就把需要的屬性放進(jìn)去即可。
然后循環(huán)數(shù)據(jù)源,使用反射通過(guò)PropertyInfo獲取該屬性的值,最后組成一個(gè)ExpandoObject,再把這個(gè)ExpandoObject放到結(jié)果集合里面即可。
接下來(lái)修改參數(shù)類,因?yàn)檫@是個(gè)通用的東西,那就是為PaginationBase添加一個(gè)Fields屬性吧:
最后修改Action方法:
測(cè)試:
好用的。但是返回的數(shù)據(jù)并不是camelcase的,這是因?yàn)镴SON.net串行化的ContractResolver并不適用于Dictionary。下面來(lái)處理這個(gè)問(wèn)題。
打開(kāi)Startup,在services.AddMvc()后邊添加:
這句話就是配置了JSON轉(zhuǎn)化的ContractResolver。
在測(cè)試一下:
現(xiàn)在Ok了。
處理異常
但如果API消費(fèi)者在Fields里面提供了不存在的屬性,那么就應(yīng)該返回Bad Request。
原理上我也許可以使用ProperyMappingContainer里面的驗(yàn)證方法,但是數(shù)據(jù)塑形并不使用映射表。而且目的不同,一個(gè)是排序一個(gè)是數(shù)據(jù)塑形,所以因?yàn)殛P(guān)注分離吧(SoC)。
我們要做的就是給定一個(gè)Fields和一個(gè)類型,需要判斷Fields里面包含的字段屬性在這個(gè)類型里面都存在,所以還是做一個(gè)Service比較好,可以注入使用。
看代碼:
這個(gè)類比較簡(jiǎn)單不多講了,別忘了在Startup里面注冊(cè)。
然后在Controller里面注入并使用,別忘了還需要修改CreateCountryUri方法:
測(cè)試:
OK.
對(duì)單個(gè)資源塑形
這個(gè)跟集合的原理差不多,先建立一個(gè)擴(kuò)展方法:
再修改Action即可:
測(cè)試:
?
是好用的,我就不多測(cè)試了。
?
針對(duì)數(shù)據(jù)塑形需要注意的是,盡量把Id帶上,否則可能無(wú)法獲取相關(guān)的鏈接了。
?
今天先寫(xiě)到這里,還有很多更深入一點(diǎn)的功能沒(méi)有做,我就不做了。
到目前為止,這些Web API仍然稱不上是RESTful的API,成熟度不夠高,有些約束也沒(méi)達(dá)到。下一篇文章會(huì)把升級(jí)這些API以便支持HATEOAS。
代碼在這:https://github.com/solenovex/ASP.NET-Core-2.0-RESTful-API-Tutorial
項(xiàng)目有一些文件的拜訪目錄可能不對(duì),暫時(shí)先不處理。
相關(guān)文章:
用ASP.NET Core 2.0 建立規(guī)范的 REST API -- 預(yù)備知識(shí)
用ASP.NET Core 2.0 建立規(guī)范的 REST API -- 預(yù)備知識(shí) (2) + 準(zhǔn)備項(xiàng)目
用ASP.NET Core 2.0 建立規(guī)范的 REST API -- GET 和 POST
用ASP.NET Core 2.0 建立規(guī)范的 REST API -- DELETE, UPDATE, PATCH 和 Log
原文地址: http://www.cnblogs.com/cgzl/p/9117448.html
.NET社區(qū)新聞,深度好文,歡迎訪問(wèn)公眾號(hào)文章匯總 http://www.csharpkit.com
總結(jié)
以上是生活随笔為你收集整理的用ASP.NET Core 2.1 建立规范的 REST API -- 翻页/排序/过滤等的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 在ASP.NET Core中使用brot
- 下一篇: asp.net ajax控件工具集 Au