本文所需的一些預備知識能夠看這裏: 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
項目有一些文件的拜訪目錄可能不對,暫時先不處理。