You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 22:33:13