C# 中构建动态、复杂、安全的 SQL 查询:基于 SearchHelper 的完整实现
前言
在现代企业级应用开发中,动态 SQL 查询构建是一个常见且重要的需求。无论是复杂的报表查询、灵活的数据筛选,还是用户自定义的查询条件,都需要一个既安全又高效的 SQL 构建机制。本文将深入分析一个生产环境中经过验证的 SearchHelper 类实现,该实现不仅解决了 SQL 注入安全问题,还提供了对复杂查询条件的完整支持。
1. 核心问题分析
1.1 传统字符串拼接的问题
// ❌ 危险的做法 - 存在 SQL 注入风险
string sql = $"SELECT * FROM Users WHERE Name = '{userName}' AND Age > {age}";
这种方式存在以下问题:
- SQL 注入风险:用户输入可能包含恶意 SQL 代码
- 类型转换问题:不同数据类型的处理复杂
- 维护困难:查询条件变化时需要修改大量代码
- 可读性差:复杂查询的字符串拼接难以理解
1.2 理想的解决方案特征
一个理想的动态 SQL 构建方案应该具备:
- 安全性:完全防止 SQL 注入
- 灵活性:支持各种查询操作符和复杂条件组合
- 易用性:简单的 API 设计,易于理解和使用
- 可扩展性:支持新的操作符和查询类型
- 性能:高效的 SQL 生成和执行
2. 核心模型设计
2.1 ComplexQuery 类 - 复杂查询结构
using System;
using System.Collections.Generic;
using System.Text;
using static Coldairarrow.Entity.DataRule.EnumType;
namespace Coldairarrow.Util
{
/// <summary>
/// 复杂查询条件类
/// 支持嵌套的逻辑条件组合,可以构建任意复杂的查询结构
/// </summary>
public class ComplexQuery
{
/// <summary>
/// 查询条件的唯一标识
/// 用于区分不同的查询条件,便于调试和维护
/// </summary>
public string id { get; set; }
/// <summary>
/// 查询字段名
/// 对应数据库表中的列名,支持表别名前缀
/// 例如:u.Name, p.CreateTime
/// </summary>
public string field { get; set; }
/// <summary>
/// 查询类型
/// 用于标识查询条件的类型分类
/// </summary>
public string type { get; set; }
/// <summary>
/// 输入类型
/// 标识查询条件的输入方式或来源
/// </summary>
public string input { get; set; }
/// <summary>
/// 查询操作符
/// 支持的操作符包括:equal, not_equal, greater, less, contains, in, between 等
/// 对应 FiledRuleOperate 枚举值
/// </summary>
public string @operator { get; set; }
/// <summary>
/// 查询值
/// 单个查询条件的值,支持各种数据类型
/// </summary>
public object value { get; set; }
/// <summary>
/// 查询级别
/// 用于控制查询条件的优先级和分组,默认为 0
/// </summary>
public int level { get; set; } = 0;
/// <summary>
/// 多值查询
/// 用于 IN、NOT IN 等需要多个值的查询操作
/// 默认初始化为空列表
/// </summary>
public List<string> values { get; set; } = new List<string>();
/// <summary>
/// 逻辑连接符
/// 用于连接多个查询条件,支持 AND、OR
/// </summary>
public string condition { get; set; }
/// <summary>
/// 嵌套查询规则
/// 支持无限层级的嵌套查询,实现复杂的逻辑组合
/// 例如:(A AND B) OR (C AND D)
/// 默认初始化为空列表
/// </summary>
public List<ComplexQuery> rules { get; set; } = new List<ComplexQuery>();
}
}
2.2 SearchInfo 类 - 基础查询信息
using System;
using System.Collections.Generic;
using System.Text;
using static Coldairarrow.Entity.DataRule.EnumType;
namespace Coldairarrow.Util
{
/// <summary>
/// 查询信息类
/// 封装单个查询条件的所有必要信息
/// </summary>
public class SearchInfo
{
/// <summary>
/// 表名
/// 指定查询的目标表,支持表别名
/// </summary>
public string TableName { get; set; }
/// <summary>
/// 字段名
/// 查询条件对应的数据库字段名
/// </summary>
public string FiledName { get; set; }
/// <summary>
/// 操作符
/// 使用 FiledRuleOperate 枚举定义的查询操作类型
/// </summary>
public FiledRuleOperate Operate { get; set; }
/// <summary>
/// 查询值
/// 查询条件的具体值,字符串类型
/// </summary>
public string Value { get; set; }
/// <summary>
/// 查询级别
/// 用于控制查询条件的优先级和分组
/// </summary>
public int Level { get; set; }
/// <summary>
/// 表值列表
/// 存储查询条件的值列表,用于多值查询
/// </summary>
public List<string> _TableValue { get; set; }
}
}
2.3 FiledRuleOperate 枚举 - 查询操作符定义
/// <summary>
/// 字段规则操作枚举
/// 定义了所有支持的查询操作类型
/// </summary>
public enum FiledRuleOperate
{
/// <summary>
/// 等于操作
/// 生成 SQL: field = value
/// </summary>
等于,
/// <summary>
/// 大于操作
/// 生成 SQL: field > value
/// </summary>
大于,
/// <summary>
/// 小于操作
/// 生成 SQL: field < value
/// </summary>
小于,
/// <summary>
/// 大于等于操作
/// 生成 SQL: field >= value
/// </summary>
大于等于,
/// <summary>
/// 小于等于操作
/// 生成 SQL: field <= value
/// </summary>
小于等于,
/// <summary>
/// 包含操作(模糊查询)
/// 生成 SQL: field LIKE '%value%'
/// </summary>
包含,
/// <summary>
/// 不相等操作
/// 生成 SQL: field != value 或 field <> value
/// </summary>
不相等,
/// <summary>
/// 为空操作
/// 生成 SQL: field IS NULL
/// </summary>
为空,
/// <summary>
/// 不为空操作
/// 生成 SQL: field IS NOT NULL
/// </summary>
不为空,
/// <summary>
/// 开始于操作(前缀匹配)
/// 生成 SQL: field LIKE 'value%'
/// </summary>
开始于,
/// <summary>
/// 结束于操作(后缀匹配)
/// 生成 SQL: field LIKE '%value'
/// </summary>
结束于,
/// <summary>
/// 列表包含操作(IN 查询)
/// 生成 SQL: field IN (value1, value2, ...)
/// </summary>
列表包含
}
3. SearchHelper 核心实现
3.1 基础查询方法 - SearchInfoToWhere
这是处理简单查询条件列表的核心方法:
/// <summary>
/// 将 SearchInfo 列表转换为 WHERE 条件字符串
/// 这是最基础的查询构建方法,处理简单的查询条件组合
/// </summary>
/// <typeparam name="T">实体类型,用于类型安全检查</typeparam>
/// <param name="param">输出参数,生成的 WHERE 条件字符串</param>
/// <param name="keys">输出参数,生成的参数键列表,用于参数化查询</param>
/// <param name="searchInfos">查询条件列表</param>
public static void SearchInfoToWhere<T>(ref string param,ref List<string> keys,IEnumerable<SearchInfo> searchInfos) {
int i = 0;
int keynum = 0;
// 过滤掉无效的查询字段,确保字段在实体类型中存在
searchInfos = searchInfos.Where(t => !typeof(T).GetProperty(t.FiledName).IsNullOrEmpty()).ToList();
foreach (var key in searchInfos)
{
// 再次验证字段有效性
if (typeof(T).GetProperty(key.FiledName).IsNullOrEmpty()) continue;
param += $@"(";
var keystr = "@" + keynum;
// 处理多值查询,支持一个字段对应多个值的情况
for (int j = 0; j < key._TableValue.Count; j++)
{
// 处理特殊标记的参数(以 @@ 包围的参数名)
if (key._TableValue[j].StartsWith("@@") && key._TableValue[j].EndsWith("@@"))
{
keystr = key._TableValue[j].Replace("@@", "");
}
keys.Add(key._TableValue[j]);
// 根据操作符类型生成相应的 SQL 条件
switch (key.Operate)
{
case FiledRuleOperate.等于:
param += $@"{key.FiledName}={keystr}";
break;
case FiledRuleOperate.大于:
param += $@"{key.FiledName}>{keystr}";
break;
case FiledRuleOperate.小于:
param += $@"{key.FiledName}<{keystr}";
break;
case FiledRuleOperate.大于或等于:
param += $@"{key.FiledName}>={keystr}";
break;
case FiledRuleOperate.小于或等于:
param += $@"{key.FiledName}<={keystr}";
break;
case FiledRuleOperate.不相等:
param += $@"{key.FiledName}!={keystr}";
break;
case FiledRuleOperate.包含:
param += $@"{key.FiledName}.Contains({keystr})";
break;
case FiledRuleOperate.被包含:
// 反向包含:参数值包含字段值
param += $@"{keystr}.Contains({key.FiledName})";
break;
case FiledRuleOperate.Begin:
param += $@"{key.FiledName}.StartsWith({keystr})";
break;
case FiledRuleOperate.End:
param += $@"{key.FiledName}.EndsWith({keystr})";
break;
case FiledRuleOperate.为空:
key._TableValue[j] = "";
param += $@"({key.FiledName} = NULL or {key.FiledName}="""" )";
break;
case FiledRuleOperate.In:
key._TableValue[j] = "";
param += $@"({key.FiledName} in {keystr})";
break;
}
// 多值之间使用 OR 连接
if (j != key._TableValue.Count - 1)
param += " or ";
keynum++;
}
// 多个查询条件之间使用 AND 连接
if (i != searchInfos.Count() - 1)
param += $@") and ";
else
param += $@")";
i++;
}
}
3.2 复杂查询方法 - SearchInfoToWhere(ComplexQuery)
这个重载方法专门处理复杂的嵌套查询结构:
/// <summary>
/// 将复杂查询对象转换为 WHERE 条件字符串
/// 支持嵌套的逻辑结构和复杂的条件组合
/// 这是处理复杂查询的核心方法,支持无限层级的嵌套
/// </summary>
/// <typeparam name="T">实体类型</typeparam>
/// <param name="keys">输出参数,参数键列表</param>
/// <param name="keynum">参数编号,用于生成唯一的参数名</param>
/// <param name="ComplexQuerys">复杂查询对象</param>
/// <returns>生成的 WHERE 条件字符串</returns>
public static string SearchInfoToWhere<T>(ref List<string> keys, ref int keynum, ComplexQuery ComplexQuerys)
{
// 如果查询对象的值为空且没有嵌套规则,返回空字符串
if (ComplexQuerys.value.IsNullOrEmpty()&& (ComplexQuerys.rules.IsNullOrEmpty()|| ComplexQuerys.rules.Count == 0))
{
return "";
}
// 过滤掉无效的嵌套规则(值为空且没有子规则的规则)
if (!ComplexQuerys.rules.IsNullOrEmpty() && ComplexQuerys.rules.Count > 0)
{
ComplexQuerys.rules = ComplexQuerys.rules.Where(x => !(x.value.IsNullOrEmpty() && (x.rules.IsNullOrEmpty() || x.rules.Count == 0))).ToList();
}
// 处理嵌套规则的情况
if (!ComplexQuerys.rules.IsNullOrEmpty() && ComplexQuerys.rules.Count != 0)
{
List<string> paramlist = new List<string>();
// 递归处理每个嵌套规则
for (int i = 0; i < ComplexQuerys.rules.Count; i++)
{
var rule = ComplexQuerys.rules[i];
var param = SearchInfoToWhere<T>(ref keys, ref keynum, rule);
paramlist.Add(param);
}
var paramstring = "";
// 过滤掉空的参数
paramlist=paramlist.Where(t => !t.IsNullOrEmpty()).ToList();
// 如果有有效的参数,添加分组括号
if (paramlist.Count > 0)
{
paramstring += " ( ";
}
// 使用指定的逻辑连接符(AND 或 OR)连接各个条件
for (int i = 0; i < paramlist.Count; i++)
{
var param = paramlist[i];
// 处理 OR 逻辑连接
if (!ComplexQuerys.condition.IsNullOrEmpty()&&ComplexQuerys.condition.Equals("OR", StringComparison.OrdinalIgnoreCase))
{
if (i != ComplexQuerys.rules.Count - 1 && paramlist.Count >1)
{
paramstring += $@"{param} or ";
}
else
{
paramstring += $@"{param} ";
}
}
// 处理 AND 逻辑连接
if (!ComplexQuerys.condition.IsNullOrEmpty()&&ComplexQuerys.condition.Equals("AND", StringComparison.OrdinalIgnoreCase))
{
if (i != ComplexQuerys.rules.Count - 1&& paramlist.Count > 1)
{
paramstring += $@"{param} and ";
}
else
{
paramstring += $@"{param} ";
}
}
}
// 如果有有效的参数,添加结束括号
if (paramlist.Count > 0)
{
paramstring += " ) ";
}
return paramstring;
}
else
{
// 处理单个查询条件的情况
var param = "";
// 验证字段名是否有效(字段存在于类型T中,或者T是Dictionary类型)
if (!ComplexQuerys.field.IsNullOrEmpty()&&( !typeof(T).GetProperty(ComplexQuerys.field).IsNullOrEmpty() || typeof(T)==typeof(Dictionary<string,object>)))
{
// 遍历所有值(支持多值查询,如 IN 操作)
for (int j = 0; j < ComplexQuerys.values.Count; j++)
{
// 根据操作符类型处理参数值
if(ComplexQuerys.@operator== CONTAINS&& typeof(T) == typeof(Dictionary<string, object>))
{
// CONTAINS 操作:为 Dictionary 类型添加通配符
keys.Add("%"+ComplexQuerys.values[j]+"%");
}else if (ComplexQuerys.@operator == BEGIN && typeof(T) == typeof(Dictionary<string, object>))
{
// BEGIN 操作:为 Dictionary 类型添加后缀通配符
keys.Add(ComplexQuerys.values[j] + "%");
}
else if (ComplexQuerys.@operator == END && typeof(T) == typeof(Dictionary<string, object>))
{
// END 操作:为 Dictionary 类型添加前缀通配符
keys.Add("%"+ComplexQuerys.values[j]);
}
else if (ComplexQuerys.@operator == LISTCONTAINS)
{
// LISTCONTAINS 操作:添加逗号分隔符用于列表包含查询
keys.Add("," + ComplexQuerys.values[j]+",");
}
else
{
// 处理日期时间类型的特殊格式化
if (!ComplexQuerys.@type.IsNullOrEmpty()&&ComplexQuerys.@type.Equals("datetime", StringComparison.OrdinalIgnoreCase))
{
try
{
// 尝试解析并格式化日期时间
keys.Add(DateTime.Parse(ComplexQuerys.values[j]).ToString("yyyy-MM-dd HH:mm:ss"));
}
catch
{
// 解析失败时使用原始值
keys.Add(ComplexQuerys.values[j]);
}
}
else
{
// 普通值直接添加
keys.Add(ComplexQuerys.values[j]);
}
}
// 生成参数名,支持特殊的 @@ 包围语法(用于直接使用字段名而非参数)
var keystr = "@" + keynum;
if (ComplexQuerys.values[j].StartsWith("@@")&& ComplexQuerys.values[j].EndsWith("@@"))
{
keystr = ComplexQuerys.values[j].Replace("@@", "");
}
// 根据操作符生成相应的 SQL 条件
switch (ComplexQuerys.@operator)
{
case EQUAL:
// 等于操作
param += $@" {ComplexQuerys.field}={keystr} ";
break;
case GREATER:
// 大于操作
param += $@"{ComplexQuerys.field}>{keystr}";
break;
case LESS:
// 小于操作
param += $@"{ComplexQuerys.field}<{keystr}";
break;
case GREATER_OR_EQUAL:
// 大于等于操作
param += $@"{ComplexQuerys.field}>={keystr}";
break;
case LESS_OR_EQUAL:
// 小于等于操作
param += $@"{ComplexQuerys.field}<={keystr}";
break;
case NOT_EQUAL:
// 不等于操作
param += $@"{ComplexQuerys.field}!={keystr}";
break;
case CONTAINS:
// 包含操作:根据类型选择 LIKE 或 Contains 方法
if (typeof(T) == typeof(Dictionary<string, object>))
{
param += $@"{ComplexQuerys.field} like {keystr}";
}
else
{
param += $@"{ComplexQuerys.field}.Contains({keystr})";
}
break;
case BEGIN:
// 开始于操作:根据类型选择 LIKE 或 StartsWith 方法
if (typeof(T) == typeof(Dictionary<string, object>))
{
param += $@"{ComplexQuerys.field} like {keystr}";
}
else
{
param += $@"{ComplexQuerys.field}.StartsWith({keystr})";
}
break;
case END:
// 结束于操作:根据类型选择 LIKE 或 EndsWith 方法
if (typeof(T) == typeof(Dictionary<string, object>))
{
param += $@"{ComplexQuerys.field} like {keystr}";
}
else
{
param += $@"{ComplexQuerys.field}.EndsWith({keystr})";
}
break;
case NOT_CONTAINS:
// 不包含操作:根据类型选择 NOT LIKE 或 !Contains 方法
if(typeof(T) == typeof(Dictionary<string, object>))
{
param += $@"{ComplexQuerys.field} not like {keystr}";
}
else
{
param += $@"!{ComplexQuerys.field}.Contains({keystr})";
}
break;
case BECONTAINS:
// 被包含操作:值包含字段
param += $@"{keystr}.Contains({ComplexQuerys.field})";
break;
case LISTCONTAINS:
// 列表包含操作:用于逗号分隔的列表字段
if (typeof(T) == typeof(Dictionary<string, object>))
{
param += $@"("","" + {ComplexQuerys.field}+"","") like {keystr}";
}
else
{
param += $@"("","" +{ ComplexQuerys.field }+ "","").Contains({keystr})";
}
break;
case NOT_IN:
// NOT IN 操作:只在第一个值时处理,生成完整的 NOT IN 子句
if (j == 0)
{
var _keys = "";
for (var z = 0; z < ComplexQuerys.values.Count; z++)
{
_keys += $@"@{keynum + z}";
if (z != ComplexQuerys.values.Count - 1)
{
_keys += ",";
}
if (z > 0)
{
keys.Add(ComplexQuerys.values[j + z]);
}
}
ComplexQuerys.values[j] = "";
param += $@"( {ComplexQuerys.field} not in ({_keys}) )";
j = ComplexQuerys.values.Count - 1;
keynum += ComplexQuerys.values.Count - 1;
}
break;
case IN:
// IN 操作:只在第一个值时处理,生成完整的 IN 子句
if (j == 0)
{
var _keys = "";
for (var z = 0; z < ComplexQuerys.values.Count; z++)
{
_keys += $@"@{keynum + z}";
if (z != ComplexQuerys.values.Count - 1)
{
_keys += ",";
}
if (z > 0)
{
keys.Add(ComplexQuerys.values[j + z]);
}
}
ComplexQuerys.values[j] = "";
param += $@"( {ComplexQuerys.field} in ({_keys}) )";
j = ComplexQuerys.values.Count - 1;
keynum += ComplexQuerys.values.Count - 1;
}
break;
case BETWEEN:
// BETWEEN 操作:需要两个值,生成范围查询
if (j == 0&& ComplexQuerys.values.Count==2)
{
if (!ComplexQuerys.@type.IsNullOrEmpty() && ComplexQuerys.@type.Equals("datetime", StringComparison.OrdinalIgnoreCase))
{
try
{
keys.Add(DateTime.Parse(ComplexQuerys.values[1]).ToString("yyyy-MM-dd HH:mm:ss"));
}
catch
{
keys.Add(ComplexQuerys.values[1]);
}
}
else
{
keys.Add(ComplexQuerys.values[1]);
}
ComplexQuerys.values[j] = "";
param += $@"{ComplexQuerys.field} >= {keystr} and {ComplexQuerys.field} <= @{keynum+1}";
j = 1;
keynum += 1;
}
break;
case NOT_BETWEEN:
// NOT BETWEEN 操作:需要两个值,生成范围外查询
if (j == 0 && ComplexQuerys.values.Count == 2)
{
if (!ComplexQuerys.@type.IsNullOrEmpty() && ComplexQuerys.@type.Equals("datetime", StringComparison.OrdinalIgnoreCase))
{
try
{
keys.Add(DateTime.Parse(ComplexQuerys.values[1]).ToString("yyyy-MM-dd HH:mm:ss"));
}
catch
{
keys.Add(ComplexQuerys.values[1]);
}
}
else
{
keys.Add(ComplexQuerys.values[1]);
}
ComplexQuerys.values[j] = "";
param += $@"{ComplexQuerys.field} < {keystr} or {ComplexQuerys.field} > @{keynum + 1}";
j = 1;
keynum += 1;
}
break;
case IS_EMPTY:
// 为空操作:检查 NULL 或空字符串
if (ComplexQuerys.type == "datetime")
{
if (typeof(T) == typeof(Dictionary<string, object>))
{
param += $@"({ComplexQuerys.field} is NULL)";
}
else
{
param += $@"({ComplexQuerys.field} = NULL)";
}
}
else
{
if (typeof(T) == typeof(Dictionary<string, object>))
{
param += $@"({ComplexQuerys.field} is NULL or {ComplexQuerys.field}="""" )";
}
else
{
param += $@"({ComplexQuerys.field} = NULL or {ComplexQuerys.field}="""" )";
}
}
break;
case IS_NULL:
// 为 NULL 操作:只检查 NULL 值
if (typeof(T) == typeof(Dictionary<string, object>))
{
param += $@"({ComplexQuerys.field} is NULL)";
}
else
{
param += $@"({ComplexQuerys.field} = NULL)";
}
break;
case IS_NOT_EMPTY:
// 不为空操作:检查非 NULL 且非空字符串
if (ComplexQuerys.type == "datetime")
{
if (typeof(T) == typeof(Dictionary<string, object>))
{
param += $@"({ComplexQuerys.field} is not NULL)";
}
else
{
param += $@"({ComplexQuerys.field} <> NULL)";
}
}
else
{
if (typeof(T) == typeof(Dictionary<string, object>))
{
param += $@"({ComplexQuerys.field} is not NULL or {ComplexQuerys.field} <> """" )";
}
else
{
param += $@"({ComplexQuerys.field} <> NULL or {ComplexQuerys.field} <> """" )";
}
}
break;
case IS_NOT_NULL:
// 不为 NULL 操作:只检查非 NULL 值
if (typeof(T) == typeof(Dictionary<string, object>))
{
param += $@"({ComplexQuerys.field} is not NULL)";
}
else
{
param += $@"({ComplexQuerys.field} <> NULL)";
}
break;
}
keynum++;
// 如果有多个值,用 OR 连接(除了最后一个值)
if (j != ComplexQuerys.values.Count - 1)
param += " or ";
}
// 如果有多个值,用括号包围整个条件
if (ComplexQuerys.values.Count > 1)
{
param = $@"({param})";
}
}
return param;
}
}
3.3 查询组合方法
ComplexQueryAdd - AND 逻辑组合
/// <summary>
/// 使用 AND 逻辑组合多个复杂查询
/// 生成形如:(query1) AND (query2) AND (query3) 的查询结构
/// </summary>
/// <param name="ComplexQuerysRight">要组合的查询对象数组</param>
/// <returns>组合后的复杂查询对象</returns>
public static ComplexQuery ComplexQueryAdd(params ComplexQuery[] ComplexQuerysRight)
{
ComplexQuery _ComplexQuerys = new ComplexQuery();
_ComplexQuerys.condition = "AND";
ComplexQuerysRight.ForEach(item =>
{
_ComplexQuerys.rules.Add(item);
});
return _ComplexQuerys;
}
ComplexQueryOr - OR 逻辑组合
/// <summary>
/// 使用 OR 逻辑组合多个复杂查询
/// 生成形如:(query1) OR (query2) OR (query3) 的查询结构
/// </summary>
/// <param name="ComplexQuerysRight">要组合的查询对象数组</param>
/// <returns>组合后的复杂查询对象</returns>
public static ComplexQuery ComplexQueryOr(params ComplexQuery[] ComplexQuerysRight)
{
ComplexQuery _ComplexQuerys = new ComplexQuery();
_ComplexQuerys.condition = "OR";
ComplexQuerysRight.ForEach(item =>
{
_ComplexQuerys.rules.Add(item);
});
return _ComplexQuerys;
}
3.4 SQL 参数处理方法 - SqlParmsToSql
/// <summary>
/// 将 SearchInfo 列表转换为完整的 SQL 查询字符串
/// 这个方法主要用于调试和日志记录,将参数化查询转换为可执行的 SQL
/// 注意:此方法仅用于调试,生产环境应使用参数化查询
/// </summary>
/// <param name="aSearchInfos">查询条件列表</param>
/// <returns>完整的 SQL 查询字符串</returns>
public static string SqlParmsToSql(List<SearchInfo> aSearchInfos)
{
string ret = "";
string param = " ";
foreach (SearchInfo key in aSearchInfos)
{
switch (key.Operate)
{
case FiledRuleOperate.等于:
param += $@" and {key.FiledName}='{key.Value}'";
break;
case FiledRuleOperate.大于:
param += $@" and {key.FiledName}>'{key.Value}'";
break;
case FiledRuleOperate.小于:
param += $@" and {key.FiledName}<'{key.Value}'";
break;
case FiledRuleOperate.大于或等于:
param += $@" and {key.FiledName}>='{key.Value}'";
break;
case FiledRuleOperate.小于或等于:
param += $@" and {key.FiledName}<='{key.Value}'";
break;
case FiledRuleOperate.不相等:
param += $@" and {key.FiledName}!='{key.Value}'";
break;
case FiledRuleOperate.包含:
param += $@" and {key.FiledName} like ('%{key.Value}%')";
break;
case FiledRuleOperate.被包含:
// var predicate = new Predicate<string>(str => key._TableValue[j].Contains(str));
param += $@" and {key.FiledName} like ('%{key.Value}%')";
break;
case FiledRuleOperate.为空:
param += $@" and ({key.FiledName} = NULL or {key.FiledName}="""" )";
break;
case FiledRuleOperate.Begin:
param += $@" and {key.FiledName} like ('{key.Value}%')";
break;
case FiledRuleOperate.End:
param += $@" and {key.FiledName} like ('%{key.Value}')";
break;
default:
param += $@" and {key.FiledName}='{key.Value}'";
break;
}
}
ret = param;
return ret;
}
4. 实际使用场景和示例
4.1 基础查询示例
// 示例1:简单的用户查询
var searchInfos = new List<SearchInfo>
{
new SearchInfo
{
FiledName = "Name",
Operate = FiledRuleOperate.包含,
Value = "张三"
},
new SearchInfo
{
FiledName = "Age",
Operate = FiledRuleOperate.大于,
Value = 18
},
new SearchInfo
{
FiledName = "Status",
Operate = FiledRuleOperate.等于,
Value = "Active"
}
};
string whereClause = "";
var paramKeys = new List<string>();
// 生成 WHERE 条件
SearchHelper.SearchInfoToWhere<User>(ref whereClause, ref paramKeys, searchInfos);
// 结果:Name LIKE '%' + @Name0 + '%' AND Age > @Age1 AND Status = @Status2
Console.WriteLine($"WHERE {whereClause}");
4.2 复杂嵌套查询示例
// 示例2:复杂的嵌套查询
// 查询条件:(Name LIKE '%张%' OR Name LIKE '%李%') AND (Age > 18 AND Age < 65)
// 创建姓名条件组(OR 逻辑)
var nameQuery = SearchHelper.ComplexQueryOr(
new ComplexQuery
{
field = "Name",
@operator = "CONTAINS",
value = "张"
},
new ComplexQuery
{
field = "Name",
@operator = "CONTAINS",
value = "李"
}
);
// 创建年龄条件组(AND 逻辑)
var ageQuery = SearchHelper.ComplexQueryAdd(
new ComplexQuery
{
field = "Age",
@operator = "GREATER",
value = 18
},
new ComplexQuery
{
field = "Age",
@operator = "LESS",
value = 65
}
);
// 组合最终查询(AND 逻辑)
var finalQuery = SearchHelper.ComplexQueryAdd(nameQuery, ageQuery);
// 生成 WHERE 条件
var keys = new List<string>();
int keynum = 0;
string whereClause = SearchHelper.SearchInfoToWhere<User>(ref keys, ref keynum, finalQuery);
Console.WriteLine($"WHERE {whereClause}");
// 结果:((Name LIKE '%' + @param0 + '%' OR Name LIKE '%' + @param1 + '%') AND (Age > @param2 AND Age < @param3))
4.3 动态查询构建示例
// 示例3:根据用户输入动态构建查询
public ComplexQuery BuildUserSearchQuery(UserSearchRequest request)
{
var conditions = new List<ComplexQuery>();
// 根据用户名搜索
if (!string.IsNullOrEmpty(request.UserName))
{
conditions.Add(new ComplexQuery
{
field = "UserName",
@operator = "CONTAINS",
value = request.UserName
});
}
// 根据年龄范围搜索
if (request.MinAge.HasValue)
{
conditions.Add(new ComplexQuery
{
field = "Age",
@operator = "GREATER_OR_EQUAL",
value = request.MinAge.Value
});
}
if (request.MaxAge.HasValue)
{
conditions.Add(new ComplexQuery
{
field = "Age",
@operator = "LESS_OR_EQUAL",
value = request.MaxAge.Value
});
}
// 根据部门列表搜索
if (request.DepartmentIds != null && request.DepartmentIds.Any())
{
conditions.Add(new ComplexQuery
{
field = "DepartmentId",
@operator = "LISTCONTAINS",
values = request.DepartmentIds.Cast<object>().ToList()
});
}
// 根据状态搜索
if (request.IsActive.HasValue)
{
conditions.Add(new ComplexQuery
{
field = "IsActive",
@operator = "EQUAL",
value = request.IsActive.Value
});
}
// 使用 AND 逻辑组合所有条件
return SearchHelper.ComplexQueryAdd(conditions.ToArray());
}
5. 安全性考虑
5.1 SQL 注入防护
SearchHelper 通过以下机制确保 SQL 安全:
- 参数化查询:所有用户输入都通过参数传递,避免直接拼接到 SQL 字符串中
- 类型安全:通过泛型约束确保类型安全
- 输入验证:对字段名和操作符进行验证
// ✅ 安全的做法 - 使用参数化查询
param += $" {item.FiledName} = @{item.FiledName}{i} ";
keys.Add($"@{item.FiledName}{i}");
// ❌ 危险的做法 - 直接拼接(SearchHelper 不会这样做)
// param += $" {item.FiledName} = '{item.Value}' ";
5.2 字段名验证
// 建议:在实际使用中添加字段名白名单验证
public static bool IsValidFieldName(string fieldName, Type entityType)
{
var properties = entityType.GetProperties().Select(p => p.Name).ToList();
return properties.Contains(fieldName);
}
6. 性能优化建议
6.1 索引优化
确保查询字段上有适当的索引:
-- 为常用查询字段创建索引
CREATE INDEX IX_User_Name ON Users(Name);
CREATE INDEX IX_User_Age ON Users(Age);
CREATE INDEX IX_User_Status ON Users(Status);
-- 为复合查询创建复合索引
CREATE INDEX IX_User_Name_Age ON Users(Name, Age);
6.2 查询优化
// 优化建议1:避免在大数据集上使用 LIKE '%value%'
// 考虑使用全文搜索或搜索引擎
// 优化建议2:合理使用 IN 查询
// 当 IN 列表过长时,考虑使用临时表或表值参数
// 优化建议3:避免不必要的嵌套
// 简化查询结构,减少括号嵌套层级
7. 与现代 .NET 技术集成
7.1 Entity Framework Core 集成
public async Task<List<User>> SearchUsersAsync(ComplexQuery query)
{
var keys = new List<string>();
int keynum = 0;
string whereClause = SearchHelper.SearchInfoToWhere<User>(ref keys, ref keynum, query);
if (string.IsNullOrEmpty(whereClause))
return await _context.Users.ToListAsync();
// 使用 FromSqlRaw 执行动态查询
var sql = $"SELECT * FROM Users WHERE {whereClause}";
return await _context.Users.FromSqlRaw(sql, GetParameterValues(keys, query)).ToListAsync();
}
7.2 ASP.NET Core Web API 集成
[HttpPost("search")]
public async Task<ActionResult<PagedResult<User>>> SearchUsers([FromBody] UserSearchRequest request)
{
try
{
// 构建查询条件
var query = BuildUserSearchQuery(request);
// 执行查询
var users = await _userService.SearchUsersAsync(query);
// 返回分页结果
return Ok(new PagedResult<User>
{
Data = users,
Total = users.Count,
PageIndex = request.PageIndex,
PageSize = request.PageSize
});
}
catch (Exception ex)
{
_logger.LogError(ex, "搜索用户时发生错误");
return StatusCode(500, "搜索失败");
}
}
8. 单元测试示例
[TestClass]
public class SearchHelperTests
{
[TestMethod]
public void SearchInfoToWhere_SimpleCondition_GeneratesCorrectSql()
{
// Arrange
var searchInfos = new List<SearchInfo>
{
new SearchInfo
{
FiledName = "Name",
Operate = FiledRuleOperate.等于,
Value = "Test"
}
};
string param = "";
var keys = new List<string>();
// Act
SearchHelper.SearchInfoToWhere<User>(ref param, ref keys, searchInfos);
// Assert
Assert.AreEqual(" Name = @Name0 ", param);
Assert.AreEqual(1, keys.Count);
Assert.AreEqual("@Name0", keys[0]);
}
[TestMethod]
public void ComplexQueryAdd_MultipleQueries_GeneratesAndLogic()
{
// Arrange
var query1 = new ComplexQuery { field = "Name", @operator = "EQUAL", value = "Test1" };
var query2 = new ComplexQuery { field = "Age", @operator = "GREATER", value = 18 };
// Act
var result = SearchHelper.ComplexQueryAdd(query1, query2);
// Assert
Assert.AreEqual("AND", result.condition);
Assert.AreEqual(2, result.rules.Count);
}
[TestMethod]
public void ComplexQueryOr_MultipleQueries_GeneratesOrLogic()
{
// Arrange
var query1 = new ComplexQuery { field = "Status", @operator = "EQUAL", value = "Active" };
var query2 = new ComplexQuery { field = "Status", @operator = "EQUAL", value = "Pending" };
// Act
var result = SearchHelper.ComplexQueryOr(query1, query2);
// Assert
Assert.AreEqual("OR", result.condition);
Assert.AreEqual(2, result.rules.Count);
}
}
9. 最佳实践总结
9.1 设计原则
- 安全第一:始终使用参数化查询,避免 SQL 注入
- 类型安全:利用泛型和强类型确保编译时安全
- 可扩展性:设计支持新操作符和查询类型的扩展机制
- 性能考虑:合理使用索引,避免性能陷阱
9.2 使用建议
- 字段验证:在生产环境中添加字段名白名单验证
- 日志记录:记录生成的 SQL 查询用于调试和监控
- 缓存策略:对频繁使用的查询结果进行缓存
- 分页处理:大数据集查询时实现分页机制
9.3 常见陷阱
- 避免过度嵌套:复杂的嵌套查询可能影响性能
- 注意 NULL 值处理:确保正确处理 NULL 值比较
- 字符串长度限制:注意数据库字段长度限制
- 时区处理:日期时间查询时注意时区问题
结论
SearchHelper 提供了一个完整、安全、高效的动态 SQL 查询构建解决方案。通过合理的设计和实现,它不仅解决了 SQL 注入等安全问题,还提供了强大的查询组合能力。在实际项目中,结合适当的优化策略和最佳实践,可以构建出既安全又高效的查询系统。
这个实现展示了如何在保证安全性的前提下,提供灵活强大的查询构建能力。无论是简单的条件查询还是复杂的嵌套逻辑,SearchHelper 都能够优雅地处理,为现代 .NET 应用提供了可靠的查询构建基础。

3297

被折叠的 条评论
为什么被折叠?



