ASP.NET Core 9 Web API全局分页:拦截LINQ表达式树添加Skip/Take
全局LINQ层面实现ASP.NET Core 9 Web API分页方案
针对你的需求,直接在LINQ表达式树生成阶段插入Skip()和Take()是更优雅、跨数据库的方案,以下提供两种可行实现:
方案一:EF Core表达式树拦截器(全局自动分页)
利用EF Core的IQueryExpressionInterceptor在SQL生成前修改LINQ表达式,自动为所有查询添加分页逻辑,适配所有EF Core支持的数据库。
1. 定义分页参数与中间件
先从请求中提取分页参数,存入HttpContext供后续使用:
// 分页参数模型 public class PaginationParams { public int Page { get; set; } = 1; public int PageSize { get; set; } = 10; public int Skip => (Page - 1) * PageSize; } // 分页参数解析中间件 public class PaginationMiddleware { private readonly RequestDelegate _next; public PaginationMiddleware(RequestDelegate next) => _next = next; public async Task InvokeAsync(HttpContext context) { if (int.TryParse(context.Request.Query["page"], out var page) && page > 0) context.Items["Page"] = page; if (int.TryParse(context.Request.Query["pageSize"], out var pageSize) && pageSize > 0 && pageSize <= 100) context.Items["PageSize"] = pageSize; await _next(context); } }
2. 实现表达式树拦截器
创建拦截器,在查询编译阶段插入Skip()和Take():
using Microsoft.EntityFrameworkCore.Diagnostics; using System.Linq.Expressions; using System.Reflection; public class GlobalPaginationInterceptor : IQueryExpressionInterceptor { private readonly IHttpContextAccessor _httpContextAccessor; public GlobalPaginationInterceptor(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor = httpContextAccessor; public Expression QueryCompilationStarting(Expression queryExpression, QueryExpressionEventData eventData) { var httpContext = _httpContextAccessor.HttpContext; if (httpContext == null) return queryExpression; // 获取分页参数 var page = httpContext.Items["Page"] as int? ?? 1; var pageSize = httpContext.Items["PageSize"] as int? ?? 10; var skipCount = (page - 1) * pageSize; // 仅对IQueryable类型的查询应用分页,跳过单实体查询(如FirstOrDefault) if (queryExpression.Type.IsGenericType && queryExpression.Type.GetGenericTypeDefinition() == typeof(IQueryable<>)) { var elementType = queryExpression.Type.GetGenericArguments()[0]; // 获取Queryable的Skip/Take泛型方法 var skipMethod = typeof(Queryable).GetMethod(nameof(Queryable.Skip))! .MakeGenericMethod(elementType); var takeMethod = typeof(Queryable).GetMethod(nameof(Queryable.Take))! .MakeGenericMethod(elementType); // 构建Skip表达式 var skipExpr = Expression.Call(null, skipMethod, queryExpression, Expression.Constant(skipCount)); // 构建Take表达式 var takeExpr = Expression.Call(null, takeMethod, skipExpr, Expression.Constant(pageSize)); return takeExpr; } return queryExpression; } }
3. 注册服务与中间件
在Program.cs中注册拦截器、Http上下文访问器和分页中间件:
builder.Services.AddHttpContextAccessor(); // 注册DbContext并添加拦截器 builder.Services.AddDbContext<YourDbContext>(options => { options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")) .AddInterceptors(new GlobalPaginationInterceptor( builder.Services.BuildServiceProvider().GetRequiredService<IHttpContextAccessor>())); }); // 启用分页中间件 app.UseMiddleware<PaginationMiddleware>();
注意事项
- 仓储层方法应返回
IQueryable<T>而非直接执行ToListAsync(),否则表达式树已编译为SQL,拦截器无法生效:// 推荐写法 public IQueryable<Item> GetItems() => _context.Items; - 可添加特殊逻辑:比如为需要跳过分页的查询标记特性,在拦截器中判断并跳过处理。
方案二:通用仓储基类(手动统一分页)
若不想使用拦截器,可通过通用仓储基类封装分页逻辑,强制所有仓储方法遵循分页规则:
1. 通用仓储基类
public abstract class BaseRepository<T> where T : class { protected readonly YourDbContext _context; private readonly IHttpContextAccessor _httpContextAccessor; public BaseRepository(YourDbContext context, IHttpContextAccessor httpContextAccessor) { _context = context; _httpContextAccessor = httpContextAccessor; } protected IQueryable<T> ApplyPagination(IQueryable<T> query) { var httpContext = _httpContextAccessor.HttpContext; if (httpContext == null) return query; var page = httpContext.Items["Page"] as int? ?? 1; var pageSize = httpContext.Items["PageSize"] as int? ?? 10; return query.Skip((page - 1) * pageSize).Take(pageSize); } public async Task<IEnumerable<T>> GetAllAsync() { return await ApplyPagination(_context.Set<T>()).ToListAsync(); } }
2. 业务仓储继承基类
public class ItemRepository : BaseRepository<Item>, IItemRepository { public ItemRepository(YourDbContext context, IHttpContextAccessor httpContextAccessor) : base(context, httpContextAccessor) { } public async Task<IEnumerable<Item>> GetItems() { return await ApplyPagination(_context.Items).ToListAsync(); } }
两种方案均基于LINQ原生方法,EF Core会自动转换为对应数据库的分页语法(如SQL Server的OFFSET/FETCH、MySQL的LIMIT),无需手动修改SQL语句。
内容的提问来源于stack exchange,提问作者cembo
相关产品推荐
相关产品推荐

