如何用ASP.NET Core和EF Core打造灵活可扩展的动态分页系统?
摘要:引言 欢迎阅读,这篇文章主要面向初级开发者。 在开始之前,先问你一个问题:你做的系统,是不是每次增加一个查询条件或者排序字段,都要去请求参数对象里加一个属性,然后再跑去改 EF Core 的查询逻辑? 如果是,那这篇文章应该对你有用。我会带
引言
欢迎阅读,这篇文章主要面向初级开发者。
在开始之前,先问你一个问题:你做的系统,是不是每次增加一个查询条件或者排序字段,都要去请求参数对象里加一个属性,然后再跑去改 EF Core 的查询逻辑?
如果是,那这篇文章应该对你有用。我会带你做一个统一的、扩展起来不那么麻烦的分页查询方案。整体思路是四件事:统一入参、统一出参、动态排序、动态过滤。
统一请求参数
先定义一个公共的 QueryParameters 解决这个问题:
public class QueryParameters
{
private const int MaxPageSize = 100;
private int _pageSize = 10;
public int PageNumber { get; set; } = 1;
// 限制最大值,防止前端传一个很大数值把数据库搞崩了
public int PageSize
{
get => _pageSize;
set => _pageSize = value > MaxPageSize ? MaxPageSize : value;
}
// 支持多字段排序,格式:"name desc,price asc"
public string? SortBy { get; set; }
// 通用关键词搜索
public string? Search { get; set; }
// 动态过滤条件
public List<FilterItem> Filters { get; set; } = [];
// 要返回的字段,逗号分隔:"id,name,price",不传则返回全部
public string? Fields { get; set; }
}
ASP.NET Core 的模型绑定会自动把 query string 映射到这个对象,不需要手动解析。后续如果某个接口有额外参数,继承它加字段就行,不用每次从头定义。
统一响应包装器
返回值也统一一下,把分页信息和数据放在一起,调用方就不用自己拼了:
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 是任意类型的集合,用 IReadOnlyList 防止被意外修改。TotalPages、HasNextPage 和 HasPreviousPage 三个是计算属性,不需要单独赋值。
扩展方法
把分页、排序、过滤都做成 IQueryable<T> 的扩展方法,用起来像链式调用,调用的地方看起来会很干净。
分页
public static IQueryable<T> ApplyPagination<T>(
this IQueryable<T> query,
int pageNumber,
int pageSize)
{
return query
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize);
}
动态排序
解析 "name desc,price asc" 这样的字符串,动态生成排序表达式。
