ASP.NET Core Web API基于RESTFul APIs的集合结果过滤和分页
譯者薦語:如何在RESTFul APIs中進(jìn)行集合結(jié)果分頁(yè)?還是用客戶端來拼接鏈接地址么?
原文來自互聯(lián)網(wǎng),由長(zhǎng)沙DotNET技術(shù)社區(qū)【鄒溪源】翻譯。如譯文侵犯您的版權(quán),請(qǐng)聯(lián)系小編,小編將在24小時(shí)內(nèi)刪除。
在ASP.NET Core WebApi項(xiàng)目中分頁(yè)響應(yīng)數(shù)據(jù)
REST API的分頁(yè)響應(yīng)和通過REST API端點(diǎn)篩選返回的數(shù)據(jù)(它們經(jīng)常一起出現(xiàn))同樣重要。
就像過濾一樣,分頁(yè)會(huì)限制從端點(diǎn)返回的數(shù)據(jù)量,從而節(jié)省了客戶端和服務(wù)器端資源。想象一下,如果你想返回一個(gè)客戶的數(shù)據(jù),但是卻返回了所有客戶的數(shù)據(jù),或者你返回了所有的分頁(yè)數(shù)據(jù),而你搜索的數(shù)據(jù)實(shí)際上就在前幾條記錄中。
這將僅導(dǎo)致服務(wù)器上處理能力和網(wǎng)絡(luò)帶寬的浪費(fèi),也給客戶端的實(shí)現(xiàn)帶來了不必要的復(fù)雜度。
有許多技術(shù)可以解決這兩個(gè)問題,例如OData[1]或?GraphQL[2]。但是,也可以僅使用依賴于過濾和分頁(yè)API數(shù)據(jù)的定制解決方案來解決這些問題。
為什么要考慮使用定制解決方案而不是現(xiàn)有技術(shù)?原因很簡(jiǎn)單,因?yàn)樗梢员苊饽膱F(tuán)隊(duì)浪費(fèi)時(shí)間學(xué)習(xí)新技術(shù),而他們已經(jīng)可以在沒有新技術(shù)的情況下解決這些問題。而且,這些技術(shù)將強(qiáng)加于客戶端的適應(yīng)能力并限制它們的相應(yīng)客戶端的使用。依賴于過濾和分頁(yè)技術(shù)也不是沒有復(fù)雜性。有時(shí)這些簡(jiǎn)單的方法會(huì)變得非常復(fù)雜,最終會(huì)成為消費(fèi)者/客戶的一個(gè)問題。
我將提到REST API設(shè)計(jì)中自定義篩選和分頁(yè)的一些支柱,特別是在使用.NET Core WEB API在REST服務(wù)中實(shí)現(xiàn)篩選和分頁(yè)時(shí)。
使過濾器易于擴(kuò)展
我在WEB API項(xiàng)目中實(shí)現(xiàn)自定義篩選和分頁(yè)時(shí)發(fā)現(xiàn)的常見錯(cuò)誤之一是將值作為單獨(dú)的參數(shù)傳遞給MVC Controller Action方法。且別說他是不是易于擴(kuò)展,這樣做使得方法簽名變得更加復(fù)雜,并且有向端點(diǎn)添加更多過濾選項(xiàng)的趨勢(shì),從而變得更加復(fù)雜。
[HttpGet] # public IActionResult Get(String term, int page, int limit) # { # //Handle filtering and paging # } #假設(shè)一段時(shí)間后,您必須擴(kuò)展端點(diǎn)以接收DateTime和Boolean參數(shù),這些參數(shù)將參與過濾。方法簽名將更改并變?yōu)?#xff1a;
[HttpGet] # public IActionResult Get(String term, DateTime minDate, Boolean includeInactive, int page, int limit) # { # //Handle filtering and paging # } #您已經(jīng)看到,一次更新后,您的方法簽名變得更加復(fù)雜,而且為了提高閱讀這段代碼,你還得將其分成兩行顯示。除非您有版本控制,否則將很難與現(xiàn)有端點(diǎn)使用者保持一致,而且由于開發(fā)者不知道增加了新的參數(shù),而由于MVC不知道如何路由請(qǐng)求,MVC將不會(huì)匹配方法簽名只會(huì)自動(dòng)給出404響應(yīng)。這意味著您必須將新的參數(shù)設(shè)置為可選參數(shù),并將其移動(dòng)到參數(shù)列表的末尾。
[HttpGet] # public IActionResult Get(String term, int page, int limit, DateTime? minDate = null, Boolean includeInactive=false) # { # //Handle filtering and paging # } #現(xiàn)在,參數(shù)分散在簽名中,使方法簽名具有混合過濾和分頁(yè)參數(shù),沒有邏輯分組或順序,因?yàn)槌朔椒ê灻袇?shù)列表的末尾,您不能擁有其他可選參數(shù)。當(dāng)您意識(shí)到必須將這些更改應(yīng)用到不止一種方法時(shí),復(fù)雜性就會(huì)大大增加。您可能必須同時(shí)對(duì)多個(gè)端點(diǎn)進(jìn)行更改,這不僅使一種方法變得困難,而且?guī)缀跏拐麄€(gè)應(yīng)用程序外觀都難以維護(hù)。
我認(rèn)為我們已經(jīng)提出了足夠多的觀點(diǎn),可以得出結(jié)論,對(duì)過濾方法使用多個(gè)參數(shù)是一個(gè)壞主意。更好的方法是使用模型,并將所有參數(shù)作為POCO類的屬性。盡管方法仍然是HTTP GET,但是MVC可以通過使用模型的[FromQuery]關(guān)鍵字從查詢字符串中為您綁定模型。
public class FilterModel # { # public String Term { get; set; } # public DateTime MinDate { get; set; } # public Boolean IncludeInactive { get; set; } # public int Page { get; set; } # public int Limit { get; set; } # } # # [HttpGet] # public IActionResult Get([FromQuery] FilterModel filter) # { # //Handle filtering and paging # } #現(xiàn)在,擴(kuò)展過濾器是單個(gè)類的責(zé)任,如果您的過濾器在整個(gè)項(xiàng)目中是通用的,或者它具有公共屬性(例如頁(yè)碼和頁(yè)面大小/限制),則可以將其帶到基類,并且如果需要擴(kuò)展跨多個(gè)Actions甚至跨多個(gè)Controller的過濾器模型,您只需擴(kuò)展基本模型過濾器類即可。您仍然必須在過濾邏輯中處理新參數(shù),但是方法簽名將保持不變,而無需擴(kuò)展它。
不要讓客戶端為您分擔(dān)工作
我看到的許多自定義實(shí)現(xiàn)都讓客戶端形成查詢字符串以獲取下一頁(yè)。我認(rèn)為這不是正確的方法。我看到的并且我真的很喜歡的一種實(shí)現(xiàn)[3]是ZenDesk API[4]使用的這種。除了實(shí)體集合以外,響應(yīng)還包括結(jié)果下一頁(yè)和上一頁(yè)的URL。樣本響應(yīng)將是這樣的
{ # persons:[ # { # name: "John Smith", # dob: "1984-10-31", # email: "john@smith.test.com" # }, # ... # ], # nextPage: "http://localhost:5000/api/persons?name=John&page=2&limit=100", # previousPage: null # } #這樣,您的客戶就不必確定下一個(gè)頁(yè)面URL是什么,并且在采用當(dāng)今的現(xiàn)代無限滾動(dòng)方式的大多數(shù)UI實(shí)現(xiàn)(包括Web和移動(dòng))上,這種方法非常理想,因?yàn)槊總€(gè)頁(yè)面滾動(dòng)到底部都是一種新方法HTTP GET到下一頁(yè)URL。在WEB API中,看起來像這樣
public class FilterModel # { # public String Term { get; set; } # public DateTime MinDate { get; set; } # public Boolean IncludeInactive { get; set; } # public int Page { get; set; } # public int Limit { get; set; } # } # # public class PagedCollectionResponse<T> where T : class # { # public IEnumerable<T> Items { get; set; } # public Uri NextPage { get; set; } # public Uri PreviousPage { get; set; } # } # # public class Person # { # public String Name { get; set; } # public DateTime DOB { get; set; } # public String Email { get; set; } # } # # [HttpGet] # public ActionResult<PagedCollectionResponse<Person>> Get([FromQuery] FilterModel filter) # { # //Handle filtering and paging # } #這只是過濾器動(dòng)作簽名的外觀的淺層結(jié)構(gòu)。接下來,我將用一段簡(jiǎn)單的代碼解釋如何在ASP.NET Core WEB API示例控制器中實(shí)現(xiàn)這種方法。
過濾和分頁(yè)的簡(jiǎn)單示例
為了向您展示如何使用頁(yè)面指針URL來實(shí)現(xiàn)上述分頁(yè)方法,我將使用一個(gè)簡(jiǎn)單的控制器和字符串的靜態(tài)集合。理想情況下,您將查詢存儲(chǔ)庫(kù)中的數(shù)據(jù),但是為了使事情保持簡(jiǎn)單并專注于生成所描述的響應(yīng)結(jié)構(gòu),我將堅(jiān)持簡(jiǎn)單的字符串集合。
在我們跳到邏輯之前,第一件事就是創(chuàng)建模型。由于我們項(xiàng)目中的所有過濾器都會(huì)接收頁(yè)面和限制值,因此有必要將其設(shè)為抽象類,以便任何具有頁(yè)面調(diào)度的過濾器都可以繼承它。
namespace Sample.Web.Api.Models # { # public abstract class FilterModelBase:ICloneable # { # public int Page { get; set; } public int Limit { get; set; } public FilterModelBase() { this.Page = 1; this.Limit = 100; } public abstract object Clone(); } }我們有一個(gè)默認(rèn)的構(gòu)造函數(shù),它將頁(yè)面大小(Limit屬性)設(shè)置為100,這意味著默認(rèn)情況下,任何過濾器模型都將分頁(yè)顯示100個(gè)項(xiàng)目的集合中的值。我們還實(shí)現(xiàn)了ICloneable接口,但是實(shí)現(xiàn)保留為抽象,以允許繼承的類處理克隆邏輯,因?yàn)樗赡苌婕袄^承的POCO類的其他屬性。當(dāng)我們開始實(shí)現(xiàn)分頁(yè)邏輯時(shí),您將明白為什么我們需要涉及ICloneable接口。
現(xiàn)在讓我們通過繼承FilterModelBase抽象類來實(shí)現(xiàn)過濾器
public class SampleFilterModel:FilterModelBase { public string Term { get; set; } public SampleFilterModel():base() { this.Limit = 3; } public override object Clone() { var jsonString = JsonConvert.SerializeObject(this); return JsonConvert.DeserializeObject(jsonString,this.GetType()); } }除了Page和Limit屬性之外,我還添加了一個(gè)附加屬性Term,該屬性應(yīng)用于過濾我們的字符串集合。我還希望在構(gòu)造函數(shù)中將新頁(yè)面大小設(shè)置為3,而不是在基類構(gòu)造函數(shù)中分配的默認(rèn)頁(yè)面大小設(shè)置為100,這是因?yàn)橄M榭瓷倭繑?shù)據(jù)集的分頁(yè)。Clone方法表示過濾器模型實(shí)例的深層副本,使用Newtonsoft.Json[5]包通過簡(jiǎn)單的序列化/反序列化即可完成。這樣,我們涵蓋了Action方法的輸入,但是現(xiàn)在我們需要注意輸出。為了使響應(yīng)通用,我將使用相同的結(jié)構(gòu)模型,但是將根據(jù)控制器的需要更改集合的類型。為此,我使用了通用類型來聲明輸出模型,以便我們可以在多個(gè)Controller Action方法中重用它以返回不同類型的集合元素。
namespace Sample.Web.Api.Models { public class PagedCollectionResponse<T> where T:class { public IEnumerable<T> Items { get; set; } public Uri NextPage { get; set; } public Uri PreviousPage { get; set; } } }當(dāng)我們要返回上述人員的集合時(shí),我們可以使用相同的模型類來存儲(chǔ)示例數(shù)據(jù)。
namespace Sample.Web.Api.Models { public class Person { public String Name { get; set; } public DateTime DOB { get; set; } public String Email { get; set; } } }現(xiàn)在我們準(zhǔn)備寫下我們的頁(yè)面處理。正如我提到的,我將使用Person類實(shí)例的集合,并且在此演示中,我將它們聲明為在Controller構(gòu)造中啟動(dòng)的集合。
namespace Sample.Web.Api.Controllers { [Route("api/[controller]")] [ApiController] public class PersonsController : ControllerBase { IEnumerable<Person> persons = new List<Person>() { new Person() { Name = "Nancy Davolio", DOB = DateTime.Parse("1948-12-08"), Email = "nancy.davolio@test.com" }, new Person() { Name = "Andrew Fuller", DOB = DateTime.Parse("1952-02-19"), Email = "andrew.fuller@test.com" }, new Person() { Name = "Janet Leverling", DOB = DateTime.Parse("1963-08-30"), Email = "janet.leverling@test.com" }, new Person() { Name = "Margaret Peacock", DOB = DateTime.Parse("1937-09-19"), Email = "margaret.peacock@test.com" }, new Person() { Name = "Steven Buchanan", DOB = DateTime.Parse("1955-03-04"), Email = "steven.buchanan@test.com" }, new Person() { Name = "Michael Suyama", DOB = DateTime.Parse("1963-07-02"), Email = "michael.suyama@test.com" }, new Person() { Name = "Robert King", DOB = DateTime.Parse("1960-05-29"), Email = "robert.king@test.com" }, new Person() { Name = "Laura Callahan", DOB = DateTime.Parse("1958-01-09"), Email = "laura.callahan@test.com" }, new Person() { Name = "Anne Dodsworth", DOB = DateTime.Parse("1966-01-27"), Email = "anne.dodsworth@test.com" } }; // GET api/values [HttpGet] public ActionResult<PagedCollectionResponse<Person>> Get([FromQuery] SampleFilterModel filter) { //Filtering logic Func<SampleFilterModel, IEnumerable<Person>> filterData = (filterModel) => { return persons.Where(p => p.Name.StartsWith(filterModel.Term ?? String.Empty, StringComparison.InvariantCultureIgnoreCase)) .Skip((filterModel.Page-1) * filter.Limit) .Take(filterModel.Limit); }; //Get the data for the current page var result = new PagedCollectionResponse<Person>(); result.Items = filterData(filter); //Get next page URL string SampleFilterModel nextFilter = filter.Clone() as SampleFilterModel; nextFilter.Page += 1; String nextUrl = filterData(nextFilter).Count() <= 0 ? null : this.Url.Action("Get", null, nextFilter, Request.Scheme); //Get previous page URL string SampleFilterModel previousFilter = filter.Clone() as SampleFilterModel; previousFilter.Page -= 1; String previousUrl = previousFilter.Page <= 0 ? null : this.Url.Action("Get", null, previousFilter, Request.Scheme); result.NextPage = !String.IsNullOrWhiteSpace(nextUrl) ? new Uri(nextUrl) : null; result.PreviousPage = !String.IsNullOrWhiteSpace(previousUrl) ? new Uri(previousUrl) : null; return result; } } }該示例代碼非常原始,它不是生產(chǎn)代碼,它需要一些處理才能在多個(gè)控制器中重復(fù)使用,但它的目的是在簡(jiǎn)單數(shù)據(jù)收集的小樣本上生成所需的輸出數(shù)據(jù)結(jié)構(gòu)和分頁(yè)邏輯。讓我們一步一步地分析該方法的邏輯塊
?過濾邏輯?從源數(shù)據(jù)集合返回項(xiàng)目集合的Simple Func會(huì)根據(jù)傳遞的過濾器模型來獲取一批對(duì)象。此實(shí)現(xiàn)很大程度上取決于您的過濾邏輯和您要應(yīng)用過濾器的數(shù)據(jù)。Func主體特定于Action方法。?獲取當(dāng)前頁(yè)面的數(shù)據(jù)?上述Func實(shí)現(xiàn)的簡(jiǎn)單用法 。將邏輯放在Func中的原因是供以后重用以確定下一頁(yè)和上一頁(yè)URL。?獲取下一頁(yè)URL字符串?在這里,我們正在創(chuàng)建具有更新的頁(yè)碼的新模型。還記得我們使用ICloneable作為過濾器POCO嗎?現(xiàn)在,我們將使用它來創(chuàng)建深層副本并更新模型的頁(yè)碼,以便我們可以生成下一頁(yè)的URL。在生成下一頁(yè)的URL之前,我們需要知道下一頁(yè)號(hào)是否返回任何元素。我們不想在下一頁(yè)將客戶端發(fā)送到空集合響應(yīng),因?yàn)槲覀兿M蛻舳藘H依賴NextPage和PreviousPage URL屬性。?獲取上一頁(yè)URL字符串?與獲取NextPage URL非常相似,但邏輯上略有不同。我們不需要將結(jié)果集集合計(jì)為下一頁(yè)URL。我們只需要檢查頁(yè)碼是否為1,這意味著沒有更多的頁(yè)面,PreviousPage URL為空值。
我們幾乎涵蓋了所有內(nèi)容,因此讓我們看一下它在POSTMAN中的實(shí)際工作方式。
圖片在具有默認(rèn)頁(yè)面參數(shù)的初始請(qǐng)求中,我們可以看到結(jié)果集合中有3個(gè)人,我們的NextPage URL指向頁(yè)面號(hào)增加1的URL,而PreviousPage URL為null,因?yàn)槲覀冊(cè)谑醉?yè)上并且沒有之前的頁(yè)面。
如果我們遵循NextPage URL并在POSTMAN中對(duì)其執(zhí)行HTTP GET,我們將得到以下響應(yīng)。
圖片現(xiàn)在您可以看到我們同時(shí)具有NextPage URL和PreviousPage URL。如果您注意到示例數(shù)據(jù)集合中有9個(gè)元素,這意味著對(duì)NextPage URL的請(qǐng)求應(yīng)在結(jié)果集合中再給我們3個(gè)元素。
圖片我們的最后一頁(yè)在結(jié)果集中返回3個(gè)人,但是您可以注意到NextPage URL為空。這是因?yàn)轫?yè)數(shù)4的計(jì)數(shù)將在響應(yīng)中不返回任何元素,并且我們正在通知使用者沒有更多數(shù)據(jù)要返回。
我在一個(gè)簡(jiǎn)單的數(shù)據(jù)收集上演示了此WEB API響應(yīng)分頁(yè),但實(shí)際情況將涉及數(shù)據(jù)過濾和查詢數(shù)據(jù)存儲(chǔ)庫(kù)。我希望 不久的將來,我將能夠通過使用存儲(chǔ)庫(kù)模式和可重復(fù)使用的邏輯(可以應(yīng)用于多個(gè)控制器和操作而無需任何代碼重復(fù))的展示,以更加精細(xì)的實(shí)現(xiàn)編寫更詳細(xì)的文本。
References
[1]?OData:?https://www.odata.org/
[2]?GraphQL:?https://graphql.org/
[3]?實(shí)現(xiàn):?https://developer.zendesk.com/rest_api
[4]?ZenDesk API:?https://developer.zendesk.com/rest_api
[5]?Newtonsoft.Json:?https://www.newtonsoft.com/
總結(jié)
以上是生活随笔為你收集整理的ASP.NET Core Web API基于RESTFul APIs的集合结果过滤和分页的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: .NET Core开发实战(第12课:配
- 下一篇: 【译】来看看WebWindow,一个跨平