.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




