
2024年在線市場平臺的11大最佳支付解決方案
public IActionResult Get(String term, int page, int limit) #
{ #
//Handle filtering and paging #
} #
假設一段時間后,您必須擴展端點以接收DateTime和Boolean參數,這些參數將參與過濾。方法簽名將更改并變為:
[HttpGet] #
public IActionResult Get(String term, DateTime minDate, Boolean includeInactive, int page, int limit) #
{ #
//Handle filtering and paging #
} #
您已經看到,一次更新后,您的方法簽名變得更加復雜,而且為了提高閱讀這段代碼,你還得將其分成兩行顯示。除非您有版本控制,否則將很難與現有端點使用者保持一致,而且由于開發者不知道增加了新的參數,而由于MVC不知道如何路由請求,MVC將不會匹配方法簽名只會自動給出404響應。這意味著您必須將新的參數設置為可選參數,并將其移動到參數列表的末尾。
[HttpGet] #
public IActionResult Get(String term, int page, int limit, DateTime? minDate = null, Boolean includeInactive=false) #
{ #
//Handle filtering and paging #
} #
現在,參數分散在簽名中,使方法簽名具有混合過濾和分頁參數,沒有邏輯分組或順序,因為除了方法簽名中參數列表的末尾,您不能擁有其他可選參數。當您意識到必須將這些更改應用到不止一種方法時,復雜性就會大大增加。您可能必須同時對多個端點進行更改,這不僅使一種方法變得困難,而且幾乎使整個應用程序外觀都難以維護。
我認為我們已經提出了足夠多的觀點,可以得出結論,對過濾方法使用多個參數是一個壞主意。更好的方法是使用模型,并將所有參數作為POCO類的屬性。盡管方法仍然是HTTP GET,但是MVC可以通過使用模型的[FromQuery]關鍵字從查詢字符串中為您綁定模型。
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 #
} #
現在,擴展過濾器是單個類的責任,如果您的過濾器在整個項目中是通用的,或者它具有公共屬性(例如頁碼和頁面大小/限制),則可以將其帶到基類,并且如果需要擴展跨多個Actions甚至跨多個Controller的過濾器模型,您只需擴展基本模型過濾器類即可。您仍然必須在過濾邏輯中處理新參數,但是方法簽名將保持不變,而無需擴展它。
我看到的許多自定義實現都讓客戶端形成查詢字符串以獲取下一頁。我認為這不是正確的方法。我看到的并且我真的很喜歡的一種實現[3]是ZenDesk API[4]使用的這種。除了實體集合以外,響應還包括結果下一頁和上一頁的URL。樣本響應將是這樣的
{ #
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 #
} #
這樣,您的客戶就不必確定下一個頁面URL是什么,并且在采用當今的現代無限滾動方式的大多數UI實現(包括Web和移動)上,這種方法非常理想,因為每個頁面滾動到底部都是一種新方法HTTP GET到下一頁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 #
} #
這只是過濾器動作簽名的外觀的淺層結構。接下來,我將用一段簡單的代碼解釋如何在ASP.NET Core WEB API示例控制器中實現這種方法。
為了向您展示如何使用頁面指針URL來實現上述分頁方法,我將使用一個簡單的控制器和字符串的靜態集合。理想情況下,您將查詢存儲庫中的數據,但是為了使事情保持簡單并專注于生成所描述的響應結構,我將堅持簡單的字符串集合。
在我們跳到邏輯之前,第一件事就是創建模型。由于我們項目中的所有過濾器都會接收頁面和限制值,因此有必要將其設為抽象類,以便任何具有頁面調度的過濾器都可以繼承它。
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();
}
}
我們有一個默認的構造函數,它將頁面大小(Limit屬性)設置為100,這意味著默認情況下,任何過濾器模型都將分頁顯示100個項目的集合中的值。我們還實現了ICloneable接口,但是實現保留為抽象,以允許繼承的類處理克隆邏輯,因為它可能涉及繼承的POCO類的其他屬性。當我們開始實現分頁邏輯時,您將明白為什么我們需要涉及ICloneable接口。
現在讓我們通過繼承FilterModelBase抽象類來實現過濾器
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屬性之外,我還添加了一個附加屬性Term,該屬性應用于過濾我們的字符串集合。我還希望在構造函數中將新頁面大小設置為3,而不是在基類構造函數中分配的默認頁面大小設置為100,這是因為希望查看少量數據集的分頁。Clone方法表示過濾器模型實例的深層副本,使用Newtonsoft.Json[5]包通過簡單的序列化/反序列化即可完成。這樣,我們涵蓋了Action方法的輸入,但是現在我們需要注意輸出。為了使響應通用,我將使用相同的結構模型,但是將根據控制器的需要更改集合的類型。為此,我使用了通用類型來聲明輸出模型,以便我們可以在多個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; }
}
}
當我們要返回上述人員的集合時,我們可以使用相同的模型類來存儲示例數據。
namespace Sample.Web.Api.Models
{
public class Person
{
public String Name { get; set; }
public DateTime DOB { get; set; }
public String Email { get; set; }
}
}
現在我們準備寫下我們的頁面處理。正如我提到的,我將使用Person類實例的集合,并且在此演示中,我將它們聲明為在Controller構造中啟動的集合。
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;
}
}
}
該示例代碼非常原始,它不是生產代碼,它需要一些處理才能在多個控制器中重復使用,但它的目的是在簡單數據收集的小樣本上生成所需的輸出數據結構和分頁邏輯。讓我們一步一步地分析該方法的邏輯塊
?過濾邏輯 從源數據集合返回項目集合的Simple Func會根據傳遞的過濾器模型來獲取一批對象。此實現很大程度上取決于您的過濾邏輯和您要應用過濾器的數據。Func主體特定于Action方法。
?獲取當前頁面的數據 上述Func實現的簡單用法 。將邏輯放在Func中的原因是供以后重用以確定下一頁和上一頁URL。?獲取下一頁URL字符串 在這里,我們正在創建具有更新的頁碼的新模型。還記得我們使用ICloneable作為過濾器POCO嗎?現在,我們將使用它來創建深層副本并更新模型的頁碼,以便我們可以生成下一頁的URL。在生成下一頁的URL之前,我們需要知道下一頁號是否返回任何元素。我們不想在下一頁將客戶端發送到空集合響應,因為我們希望客戶端僅依賴NextPage和PreviousPage URL屬性。
?獲取上一頁URL字符串 與獲取NextPage URL非常相似,但邏輯上略有不同。我們不需要將結果集集合計為下一頁URL。我們只需要檢查頁碼是否為1,這意味著沒有更多的頁面,PreviousPage URL為空值。
我們幾乎涵蓋了所有內容,因此讓我們看一下它在POSTMAN中的實際工作方式。
在具有默認頁面參數的初始請求中,我們可以看到結果集合中有3個人,我們的NextPage URL指向頁面號增加1的URL,而PreviousPage URL為null,因為我們在首頁上并且沒有之前的頁面。
如果我們遵循NextPage URL并在POSTMAN中對其執行HTTP GET,我們將得到以下響應。
現在您可以看到我們同時具有NextPage URL和PreviousPage URL。如果您注意到示例數據集合中有9個元素,這意味著對NextPage URL的請求應在結果集合中再給我們3個元素。
我們的最后一頁在結果集中返回3個人,但是您可以注意到NextPage URL為空。這是因為頁數4的計數將在響應中不返回任何元素,并且我們正在通知使用者沒有更多數據要返回。
我在一個簡單的數據收集上演示了此WEB API響應分頁,但實際情況將涉及數據過濾和查詢數據存儲庫。我希望 不久的將來,我將能夠通過使用存儲庫模式和可重復使用的邏輯(可以應用于多個控制器和操作而無需任何代碼重復)的展示,以更加精細的實現編寫更詳細的文本。
[1]
OData: https://www.odata.org/[2]
GraphQL: https://graphql.org/[3]
實現: https://developer.zendesk.com/rest_api[4]
ZenDesk API: https://developer.zendesk.com/rest_api[5]
Newtonsoft.Json: https://www.newtonsoft.com/
本文章轉載微信公眾號@DotNET技術圈