You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

.NET Core API中可选查询参数与数据库分页过滤的优化方案

嗨,我明白你的痛点——动态拼接SQL确实风险高,而且维护起来头疼,尤其是还要处理可选参数和优先级逻辑。下面给你几个实用的解决方案,既能保证参数化安全,又能高效处理分页和过滤:

方案1:用Entity Framework Core的LINQ查询(最推荐)

如果你的项目已经在用EF Core(或者可以引入它),这绝对是最优解。LINQ会自动帮你生成参数化SQL,完全避免注入风险,而且代码逻辑清晰,维护成本极低。

假设你的实体类是Item,DbContext里有DbSet<Item> Items,可以这么写:

[HttpGet]
[Authorize(AuthenticationSchemes = AuthSchemes)]
[Route("items")]
public async Task<IActionResult> GetPagedItems(
    [FromQuery] PageParameters pageParameters,
    [FromQuery] OptionalSearchParams searchParameters)
{
    var query = _context.Items.AsQueryable();

    // 处理Field1的过滤:Contains优先级高于Equals
    if (!string.IsNullOrEmpty(searchParameters.Field1Contains))
    {
        query = query.Where(i => i.Field1.Contains(searchParameters.Field1Contains));
    }
    else if (!string.IsNullOrEmpty(searchParameters.Field1Equals))
    {
        query = query.Where(i => i.Field1 == searchParameters.Field1Equals);
    }

    // 同理处理Field2
    if (!string.IsNullOrEmpty(searchParameters.Field2Contains))
    {
        query = query.Where(i => i.Field2.Contains(searchParameters.Field2Contains));
    }
    else if (!string.IsNullOrEmpty(searchParameters.Field2Equals))
    {
        query = query.Where(i => i.Field2 == searchParameters.Field2Equals);
    }

    // 数据库层面分页:先排序,再跳过,再取指定数量
    var totalCount = await query.CountAsync();
    var pagedItems = await query
        .OrderBy(i => i.Field) // 替换成你的实际排序字段
        .Skip((pageParameters.PageNumber - 1) * pageParameters.PageSize)
        .Take(pageParameters.PageSize)
        .ToListAsync();

    return Ok(new 
    { 
        TotalItems = totalCount,
        CurrentPage = pageParameters.PageNumber,
        PageSize = pageParameters.PageSize,
        Items = pagedItems
    });
}

为什么这个方案好?

  • 绝对安全:EF Core自动为所有查询参数生成参数化SQL,彻底杜绝SQL注入。
  • 逻辑清晰:用if-else直接实现你的优先级规则,比嵌套CASE语句好读10倍。
  • 高效分页:分页逻辑(Skip/Take)会被转换成SQL的OFFSET/FETCH,完全在数据库层面处理,不会拉取全量数据。
  • 易维护:后续加新的过滤字段,只需要加一段对应的if-else即可。

方案2:用Dapper的动态参数+安全SQL构建

如果你更喜欢用原生SQL但不想拼接值,可以用Dapper的DynamicParameters来管理参数,安全地构建查询条件。

[HttpGet]
[Authorize(AuthenticationSchemes = AuthSchemes)]
[Route("items")]
public async Task<IActionResult> GetPagedItems(
    [FromQuery] PageParameters pageParameters,
    [FromQuery] OptionalSearchParams searchParameters)
{
    var sqlBuilder = new StringBuilder();
    sqlBuilder.Append(@"
        SELECT *
        FROM [SomeTable]
        WHERE 1=1 "); // 用1=1简化后续AND条件的拼接

    var parameters = new DynamicParameters();

    // 处理Field1
    if (!string.IsNullOrEmpty(searchParameters.Field1Contains))
    {
        sqlBuilder.Append("AND [Field1] LIKE @Field1Contains ");
        parameters.Add("@Field1Contains", $"%{searchParameters.Field1Contains}%");
    }
    else if (!string.IsNullOrEmpty(searchParameters.Field1Equals))
    {
        sqlBuilder.Append("AND [Field1] = @Field1Equals ");
        parameters.Add("@Field1Equals", searchParameters.Field1Equals);
    }

    // 处理Field2
    if (!string.IsNullOrEmpty(searchParameters.Field2Contains))
    {
        sqlBuilder.Append("AND [Field2] LIKE @Field2Contains ");
        parameters.Add("@Field2Contains", $"%{searchParameters.Field2Contains}%");
    }
    else if (!string.IsNullOrEmpty(searchParameters.Field2Equals))
    {
        sqlBuilder.Append("AND [Field2] = @Field2Equals ");
        parameters.Add("@Field2Equals", searchParameters.Field2Equals);
    }

    // 添加排序和分页
    sqlBuilder.Append(@"
        ORDER BY [Field]
        OFFSET @Offset ROWS
        FETCH NEXT @PageSize ROWS ONLY");

    parameters.Add("@Offset", (pageParameters.PageNumber - 1) * pageParameters.PageSize);
    parameters.Add("@PageSize", pageParameters.PageSize);

    // 执行查询并获取总条数
    using (var connection = new SqlConnection(_connectionString))
    {
        var items = await connection.QueryAsync<Item>(sqlBuilder.ToString(), parameters);
        
        // 单独查询总条数(也可以用COUNT(*) OVER()在同一个查询里返回,更高效)
        var countSql = $"SELECT COUNT(*) FROM [SomeTable] WHERE {sqlBuilder.ToString().Split("WHERE ").Last().Split("ORDER BY ").First()}";
        var totalCount = await connection.QueryFirstAsync<int>(countSql, parameters);

        return Ok(new 
        { 
            TotalItems = totalCount,
            CurrentPage = pageParameters.PageNumber,
            PageSize = pageParameters.PageSize,
            Items = items
        });
    }
}

这个方案的优势:

  • 安全参数化:所有值都通过占位符传递,没有直接拼接字符串,避免注入。
  • 灵活可控:完全掌控SQL语句,适合需要复杂原生SQL的场景。
  • 高效分页:同样是数据库层面处理分页,不会拉取全量数据。

方案3:用存储过程的可选参数

如果你的团队更倾向于用存储过程,也可以定义带默认值的参数,在存储过程内部处理过滤优先级:

首先创建存储过程:

CREATE PROCEDURE GetPagedItems
    @PageNumber INT = 1,
    @PageSize INT = 10,
    @Field1Contains NVARCHAR(MAX) = NULL,
    @Field1Equals NVARCHAR(MAX) = NULL,
    @Field2Contains NVARCHAR(MAX) = NULL,
    @Field2Equals NVARCHAR(MAX) = NULL
AS
BEGIN
    SET NOCOUNT ON;

    WITH FilteredItems AS (
        SELECT *
        FROM [SomeTable]
        WHERE 
            -- Field1:Contains优先,都为空则不过滤
            (
                (@Field1Contains IS NOT NULL AND [Field1] LIKE '%' + @Field1Contains + '%')
                OR 
                (@Field1Contains IS NULL AND @Field1Equals IS NOT NULL AND [Field1] = @Field1Equals)
                OR 
                (@Field1Contains IS NULL AND @Field1Equals IS NULL)
            )
            AND
            -- Field2:同理
            (
                (@Field2Contains IS NOT NULL AND [Field2] LIKE '%' + @Field2Contains + '%')
                OR 
                (@Field2Contains IS NULL AND @Field2Equals IS NOT NULL AND [Field2] = @Field2Equals)
                OR 
                (@Field2Contains IS NULL AND @Field2Equals IS NULL)
            )
    )

    -- 返回分页数据
    SELECT *
    FROM FilteredItems
    ORDER BY [Field]
    OFFSET (@PageNumber - 1) * @PageSize ROWS
    FETCH NEXT @PageSize ROWS ONLY;

    -- 返回总条数
    SELECT COUNT(*) AS TotalCount FROM FilteredItems;
END

然后在.NET代码里调用:

[HttpGet]
[Authorize(AuthenticationSchemes = AuthSchemes)]
[Route("items")]
public async Task<IActionResult> GetPagedItems(
    [FromQuery] PageParameters pageParameters,
    [FromQuery] OptionalSearchParams searchParameters)
{
    var parameters = new DynamicParameters();
    parameters.Add("@PageNumber", pageParameters.PageNumber);
    parameters.Add("@PageSize", pageParameters.PageSize);
    parameters.Add("@Field1Contains", searchParameters.Field1Contains ?? (object)DBNull.Value);
    parameters.Add("@Field1Equals", searchParameters.Field1Equals ?? (object)DBNull.Value);
    parameters.Add("@Field2Contains", searchParameters.Field2Contains ?? (object)DBNull.Value);
    parameters.Add("@Field2Equals", searchParameters.Field2Equals ?? (object)DBNull.Value);

    using (var connection = new SqlConnection(_connectionString))
    {
        using (var multiResult = await connection.QueryMultipleAsync(
            "GetPagedItems", 
            parameters, 
            commandType: CommandType.StoredProcedure))
        {
            var items = multiResult.Read<Item>().ToList();
            var totalCount = multiResult.ReadFirst<int>();

            return Ok(new 
            { 
                TotalItems = totalCount,
                CurrentPage = pageParameters.PageNumber,
                PageSize = pageParameters.PageSize,
                Items = items
            });
        }
    }
}

这个方案的适用场景:

  • 团队习惯用存储过程管理数据库逻辑。
  • 需要在数据库层面实现更复杂的业务规则。

内容的提问来源于stack exchange,提问作者Matt Sykes

火山引擎 最新活动