asp.net core + ef core 實(shí)現(xiàn)動(dòng)態(tài)可擴(kuò)展的分頁(yè)實(shí)踐方案
引言
歡迎閱讀,這篇文章主要面向初級(jí)開(kāi)發(fā)者。
在開(kāi)始之前,先問(wèn)你一個(gè)問(wèn)題:你做的系統(tǒng),是不是每次增加一個(gè)查詢(xún)條件或者排序字段,都要去請(qǐng)求參數(shù)對(duì)象里加一個(gè)屬性,然后再跑去改 EF Core 的查詢(xún)邏輯?
如果是,那這篇文章應(yīng)該對(duì)你有用。我會(huì)帶你做一個(gè)統(tǒng)一的、擴(kuò)展起來(lái)不那么麻煩的分頁(yè)查詢(xún)方案。整體思路是四件事:?統(tǒng)一入?yún)?、統(tǒng)一出參、動(dòng)態(tài)排序、動(dòng)態(tài)過(guò)濾?。
統(tǒng)一請(qǐng)求參數(shù)
先定義一個(gè)公共的 QueryParameters 解決這個(gè)問(wèn)題:
public class QueryParameters
{
private const int MaxPageSize = 100;
private int _pageSize = 10;
public int PageNumber { get; set; } = 1;
// 限制最大值,防止前端傳一個(gè)很大數(shù)值把數(shù)據(jù)庫(kù)搞崩了
public int PageSize
{
get => _pageSize;
set => _pageSize = value > MaxPageSize ? MaxPageSize : value;
}
// 支持多字段排序,格式:"name desc,price asc"
public string? SortBy { get; set; }
// 通用關(guān)鍵詞搜索
public string? Search { get; set; }
// 動(dòng)態(tài)過(guò)濾條件
public List<FilterItem> Filters { get; set; } = [];
// 要返回的字段,逗號(hào)分隔:"id,name,price",不傳則返回全部
public string? Fields { get; set; }
}
ASP.NET Core 的模型綁定會(huì)自動(dòng)把 query string 映射到這個(gè)對(duì)象,不需要手動(dòng)解析。后續(xù)如果某個(gè)接口有額外參數(shù),繼承它加字段就行,不用每次從頭定義。
統(tǒng)一響應(yīng)包裝器
返回值也統(tǒng)一一下,把分頁(yè)信息和數(shù)據(jù)放在一起,調(diào)用方就不用自己拼了:
public class PagedResponse<T>
{
// IReadOnlyList 防止外部隨意修改集合
public IReadOnlyList<T> Data { get; init; } = [];
public int PageNumber { get; init; }
public int PageSize { get; init; }
public int TotalRecords { get; init; }
public int TotalPages => (int)Math.Ceiling(TotalRecords / (double)PageSize);
public bool HasNextPage => PageNumber < TotalPages;
public bool HasPreviousPage => PageNumber > 1;
}
Data 是任意類(lèi)型的集合,用 IReadOnlyList 防止被意外修改。TotalPages、HasNextPage 和 HasPreviousPage 三個(gè)是計(jì)算屬性,不需要單獨(dú)賦值。
擴(kuò)展方法
把分頁(yè)、排序、過(guò)濾都做成 IQueryable<T> 的擴(kuò)展方法,用起來(lái)像鏈?zhǔn)秸{(diào)用,調(diào)用的地方看起來(lái)會(huì)很干凈。
分頁(yè)
public static IQueryable<T> ApplyPagination<T>(
this IQueryable<T> query,
int pageNumber,
int pageSize)
{
return query
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize);
}
動(dòng)態(tài)排序
解析 "name desc,price asc" 這樣的字符串,動(dòng)態(tài)生成排序表達(dá)式。用反射就能做到,不需要額外的庫(kù):
public static IQueryable<T> ApplySort<T>(
this IQueryable<T> query,
string? sortBy)
{
if (string.IsNullOrWhiteSpace(sortBy))
return query;
var orderParams = sortBy.Split(',', StringSplitOptions.RemoveEmptyEntries);
var isFirst = true;
foreach (var param in orderParams)
{
var parts = param.Trim().Split(' ');
var propertyName = parts[0];
var isDesc = parts.Length > 1
&& parts[1].Equals("desc", StringComparison.OrdinalIgnoreCase);
// 用反射找屬性,找不到就跳過(guò),避免拋異常
var prop = typeof(T).GetProperty(
propertyName,
BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
if (prop == null) continue;
// 構(gòu)建表達(dá)式樹(shù):x => x.PropertyName
var paramExpr = Expression.Parameter(typeof(T), "x");
var body = Expression.Property(paramExpr, prop);
var lambda = Expression.Lambda(body, paramExpr);
var methodName = isFirst
? (isDesc ? "OrderByDescending" : "OrderBy")
: (isDesc ? "ThenByDescending" : "ThenBy");
var method = typeof(Queryable).GetMethods()
.First(m => m.Name == methodName && m.GetParameters().Length == 2)
.MakeGenericMethod(typeof(T), prop.PropertyType);
query = (IQueryable<T>)method.Invoke(null, [query, lambda])!;
isFirst = false;
}
return query;
}
也可以考慮
System.Linq.Dynamic.Core這個(gè)庫(kù)。
動(dòng)態(tài)過(guò)濾
這是擴(kuò)展性最強(qiáng)的一塊。前端傳字段名 + 操作符 + 值,后端用表達(dá)式樹(shù)動(dòng)態(tài)拼 Where 條件,不需要每加一個(gè)篩選項(xiàng)就改后端代碼。
先定義過(guò)濾條件的數(shù)據(jù)結(jié)構(gòu):
public class FilterItem
{
// 字段名,對(duì)應(yīng)實(shí)體屬性,不區(qū)分大小寫(xiě)
public string Field { get; set; } = string.Empty;
// 操作符:eq、neq、contains、startswith、endswith、
// gt、gte、lt、lte、between、in、isnull、isnotnull
public string Op { get; set; } = "eq";
// 值,between 用逗號(hào)分隔兩個(gè)值,in 用逗號(hào)分隔多個(gè)值
public string? Value { get; set; }
}
然后實(shí)現(xiàn)過(guò)濾擴(kuò)展方法:
public static IQueryable<T> ApplyFilters<T>(
this IQueryable<T> query,
IEnumerable<FilterItem> filters)
{
foreach (var filter in filters)
{
var prop = typeof(T).GetProperty(
filter.Field,
BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
// 找不到屬性,或者沒(méi)有 [Filterable] 標(biāo)記,就跳過(guò)
if (prop == null || !prop.IsDefined(typeof(FilterableAttribute), false))
continue;
var param = Expression.Parameter(typeof(T), "x");
var member = Expression.Property(param, prop);
Expression? condition = null;
switch (filter.Op.ToLower())
{
case "eq":
condition = Expression.Equal(member, ParseConstant(filter.Value, prop.PropertyType));
break;
case "neq":
condition = Expression.NotEqual(member, ParseConstant(filter.Value, prop.PropertyType));
break;
case "gt":
condition = Expression.GreaterThan(member, ParseConstant(filter.Value, prop.PropertyType));
break;
case "gte":
condition = Expression.GreaterThanOrEqual(member, ParseConstant(filter.Value, prop.PropertyType));
break;
case "lt":
condition = Expression.LessThan(member, ParseConstant(filter.Value, prop.PropertyType));
break;
case "lte":
condition = Expression.LessThanOrEqual(member, ParseConstant(filter.Value, prop.PropertyType));
break;
case "contains":
condition = Expression.Call(
member,
typeof(string).GetMethod("Contains", [typeof(string)])!,
Expression.Constant(filter.Value ?? string.Empty));
break;
case "startswith":
condition = Expression.Call(
member,
typeof(string).GetMethod("StartsWith", [typeof(string)])!,
Expression.Constant(filter.Value ?? string.Empty));
break;
case "endswith":
condition = Expression.Call(
member,
typeof(string).GetMethod("EndsWith", [typeof(string)])!,
Expression.Constant(filter.Value ?? string.Empty));
break;
case "between":
// value 格式:"10,100"
var rangeParts = filter.Value?.Split(',') ?? [];
if (rangeParts.Length == 2)
{
var lower = ParseConstant(rangeParts[0].Trim(), prop.PropertyType);
var upper = ParseConstant(rangeParts[1].Trim(), prop.PropertyType);
condition = Expression.AndAlso(
Expression.GreaterThanOrEqual(member, lower),
Expression.LessThanOrEqual(member, upper));
}
break;
case "in":
// value 格式:"1,2,3",最多取 50 個(gè),防止 OR 鏈過(guò)長(zhǎng)
var inValues = filter.Value?.Split(',').Take(50)
.Select(v => ParseConstant(v.Trim(), prop.PropertyType))
.ToList() ?? [];
if (inValues.Count > 0)
{
condition = inValues
.Select(v => (Expression)Expression.Equal(member, v))
.Aggregate(Expression.OrElse);
}
break;
case "isnull":
condition = Expression.Equal(member, Expression.Constant(null, prop.PropertyType));
break;
case "isnotnull":
condition = Expression.NotEqual(member, Expression.Constant(null, prop.PropertyType));
break;
}
if (condition == null) continue;
var lambda = Expression.Lambda<Func<T, bool>>(condition, param);
query = query.Where(lambda);
}
return query;
}
// 把字符串值轉(zhuǎn)成對(duì)應(yīng)類(lèi)型的常量表達(dá)式
private static ConstantExpression ParseConstant(string? value, Type targetType)
{
var underlyingType = Nullable.GetUnderlyingType(targetType) ?? targetType;
if (value == null)
return Expression.Constant(null, targetType);
var converted = Convert.ChangeType(value, underlyingType);
return Expression.Constant(converted, targetType);
}
contains/startswith/endswith應(yīng)對(duì)字符串,gt/lt/between應(yīng)對(duì)對(duì)數(shù)值和日期。類(lèi)型不匹配時(shí)會(huì)拋異常,生產(chǎn)代碼里可以在這里加 try-catch,捕獲后根據(jù)情況進(jìn)行處理。
動(dòng)態(tài)返回字段
有時(shí)候列表頁(yè)只需要 id 和 name,詳情頁(yè)才需要全量字段。與其寫(xiě)兩個(gè)接口,不如讓前端自己說(shuō)想要哪些字段(我經(jīng)歷的項(xiàng)目都是后端定義好給前端哈,不是前段自己拿,前段自己也不想拿)。
思路是:查出完整的實(shí)體,然后用反射把指定字段打包成字典返回,JSON 序列化后就只有這些字段。
public static class FieldSelectorExtensions
{
public static IDictionary<string, object?> SelectFields<T>(
this T obj,
IEnumerable<string> fields)
{
var result = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
var props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (var fieldName in fields)
{
var prop = props.FirstOrDefault(p =>
p.Name.Equals(fieldName, StringComparison.OrdinalIgnoreCase));
if (prop != null)
result[prop.Name] = prop.GetValue(obj);
}
return result;
}
public static IEnumerable<IDictionary<string, object?>> SelectFields<T>(
this IEnumerable<T> items,
string? fields)
{
if (string.IsNullOrWhiteSpace(fields))
{
var allProps = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Select(p => p.Name);
return items.Select(item => item.SelectFields(allProps));
}
var fieldList = fields
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(f => f.Trim());
return items.Select(item => item.SelectFields(fieldList));
}
}
安全性:字段白名單
動(dòng)態(tài)過(guò)濾和動(dòng)態(tài)返回字段功能很方便,但不是所有字段都該暴露出去,比如密碼、證件號(hào)、客戶(hù)姓名這類(lèi)。用一個(gè)自定義 Attribute 來(lái)標(biāo)記哪些字段允許外部操作:
[AttributeUsage(AttributeTargets.Property)]
public class FilterableAttribute : Attribute { }
public class Product
{
public int Id { get; set; }
[Filterable]
public string Name { get; set; } = string.Empty;
[Filterable]
public decimal Price { get; set; }
[Filterable]
public int Stock { get; set; }
// 不加 [Filterable],外部無(wú)法通過(guò) filters 參數(shù)過(guò)濾這個(gè)字段
public string InternalRemark { get; set; } = string.Empty;
}
ApplyFilters 里已經(jīng)加了這個(gè)檢查(prop.IsDefined(typeof(FilterableAttribute), false)),找到屬性之后會(huì)先驗(yàn)證標(biāo)記,沒(méi)有就跳過(guò)。也可以反著來(lái)設(shè)計(jì),加一個(gè) FilterIgnore 特性,檢查的地方做相應(yīng)的調(diào)整。
接到 Controller 里
有了這些擴(kuò)展方法,Controller 里的邏輯就很平:
[HttpGet]
public async Task<ActionResult> GetProducts([FromQuery] QueryParameters parameters)
{
var query = _context.Products.AsQueryable();
// 動(dòng)態(tài)過(guò)濾
if (parameters.Filters.Count > 0)
query = query.ApplyFilters(parameters.Filters);
// 先算總數(shù)(必須在分頁(yè)之前)
var totalRecords = await query.CountAsync();
// 排序 + 分頁(yè)
var items = await query
.ApplySort(parameters.SortBy)
.ApplyPagination(parameters.PageNumber, parameters.PageSize)
.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price,
Stock = p.Stock
})
.ToListAsync();
// 按需返回字段
var data = items.SelectFields(parameters.Fields).ToList();
return Ok(new
{
data,
pageNumber = parameters.PageNumber,
pageSize = parameters.PageSize,
totalRecords,
totalPages = (int)Math.Ceiling(totalRecords / (double)parameters.PageSize),
hasNextPage = parameters.PageNumber < (int)Math.Ceiling(totalRecords / (double)parameters.PageSize),
hasPreviousPage = parameters.PageNumber > 1
});
}
前端請(qǐng)求示例:
# 查價(jià)格在 100-500 之間、名字包含"手機(jī)",只返回 id 和 name,按價(jià)格升序 GET /api/products ?filters[0].field=price&filters[0].op=between&filters[0].value=100,500 &filters[1].field=name&filters[1].op=contains&filters[1].value=手機(jī) &fields=id,name &sortBy=price asc &pageNumber=1&pageSize=20
返回結(jié)果:
{
"data": [
{ "Id": 1, "Name": "iPhone 16" },
{ "Id": 2, "Name": "小米 15" }
],
"pageNumber": 1,
"pageSize": 20,
"totalRecords": 2,
"totalPages": 1,
"hasNextPage": false,
"hasPreviousPage": false
}
ok,你學(xué)會(huì)了嗎?
到此這篇關(guān)于asp.net core + ef core 實(shí)現(xiàn)動(dòng)態(tài)可擴(kuò)展的分頁(yè)實(shí)踐方案的文章就介紹到這了,更多相關(guān)asp.net core ef core分頁(yè)查詢(xún)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- ASP.NET?Core使用EF查詢(xún)數(shù)據(jù)
- ASP.NET?Core?5.0中的Host.CreateDefaultBuilder執(zhí)行過(guò)程解析
- ASP.NET Core使用EF保存數(shù)據(jù)、級(jí)聯(lián)刪除和事務(wù)使用
- ASP.NET?Core使用EF為關(guān)系數(shù)據(jù)庫(kù)建模
- ASP.NET Core使用EF創(chuàng)建模型(必需和可選屬性、最大長(zhǎng)度、并發(fā)標(biāo)記、陰影屬性)
- ASP.NET?Core基于現(xiàn)有數(shù)據(jù)庫(kù)創(chuàng)建EF模型
- ASP.NET?Core使用EF?SQLite對(duì)數(shù)據(jù)庫(kù)增刪改查
相關(guān)文章
.NET中XML序列化和反序列化常用類(lèi)和屬性小結(jié)
本文主要介紹了.NET中XML序列化和反序列化常用類(lèi)和屬性小結(jié),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04
使用EF Code First搭建簡(jiǎn)易ASP.NET MVC網(wǎng)站并允許數(shù)據(jù)庫(kù)遷移
這篇文章介紹了使用EF Code First搭建簡(jiǎn)易ASP.NET MVC網(wǎng)站并允許數(shù)據(jù)庫(kù)遷移的方法,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-09-09
asp.net下一個(gè)賬號(hào)不允許多個(gè)用戶(hù)同時(shí)在線,重復(fù)登陸的代碼
asp.net下一個(gè)賬號(hào)不允許多個(gè)用戶(hù)同時(shí)在線,重復(fù)登陸的代碼,需要的朋友可以參考下。2010-10-10
asp.net session的使用與過(guò)期實(shí)例代碼
本文章來(lái)簡(jiǎn)單的介紹asp.net中session常見(jiàn)兩種用法,一種是session使用如何創(chuàng)建,另一種是告訴你如何判斷session過(guò)期了,有需要了解的朋友可以參考一下2013-08-08
JavaScript用JQuery呼叫Server端方法實(shí)現(xiàn)代碼與參考語(yǔ)法
從Javascript客戶(hù)端用JQuery呼叫Server端的方法,這也是一個(gè)大膽的嘗試,本人做了演示動(dòng)畫(huà)以及參考語(yǔ)法,感興趣的朋友可以參考下,希望本人對(duì)你有所幫助2013-01-01
ASP.NET技巧:同時(shí)對(duì)多個(gè)文件進(jìn)行大量寫(xiě)操作對(duì)性能優(yōu)化
ASP.NET技巧:同時(shí)對(duì)多個(gè)文件進(jìn)行大量寫(xiě)操作對(duì)性能優(yōu)化...2006-09-09
.NET使用Hisql實(shí)現(xiàn)菜單管理(增刪改查)
這篇文章介紹了.NET使用Hisql實(shí)現(xiàn)菜單管理(增刪改查)的方法,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-07-07

