一个基于 Dapper 的静态工具类,专为简化 Oracle 数据库的常规 CRUD 操作而设计。
它通过 “特性驱动(Attribute-Driven)” 的模式,利用反射机制将 C# 对象模型(DTO)动态转化为高性能、参数化的 SQL 语句。
主要特性:
-
🚀 零 SQL 编写 针对单表的增、删、改、查、总数统计等操作,无需手写任何 SQL 片段,完全由 DTO 结构自动生成。
-
🛡️ 自动参数化(防注入) 深度集成 Dapper 的
DynamicParameters,所有查询条件均通过占位符(如:param)传递,从根源杜绝 SQL 注入风险。 -
🔍 调试极其友好 所有构建方法均返回
SqlInfo对象,其中的DebugSql属性会将参数还原为可执行的 SQL 字符串,方便直接复制到 PL/SQL 中排查逻辑。 -
⚙️ 灵活的元数据映射 支持自定义表名映射、列名映射、主键识别,允许在 DTO 上直接定义复杂的 Oracle 函数。
| 特性 | 作用 | 必填 | 说明 |
[Table("Name")] |
映射数据库表名 | 是 | 如果不加,调用方法时需显式传入表名参数。 |
[Key] |
标识主键 | 是 | 用于 UPDATE 的 WHERE 条件及 INSERT 的序列判断。 |
[Column("Name")] |
映射数据库列名 | 否 | 属性名与列名一致时可省略。 |
[NotMapped] |
排除字段 | 否 | 该属性完全不参与 SQL 生成(如辅助字段)。 |
[Select("...")] |
虚拟计算列 | 否 | 用于 SELECT 查询时的函数列(如格式化日期、子查询),不参与保存。 |
[Query("...")] |
虚拟查询列 | 否 | 用于 SELECT 查询时的函数的WHERE条件过滤,通常用于时间的等条件 |
2026年2月4日 | V1.0
版本说明:实现 Dapper 与实体模型的初步解耦,解决基础 CRUD 自动构建问题。
-
核心功能实现:
-
基础映射: 支持
[Table]、[Column]、[Key]、[NotMapped]特性,建立 DTO 与物理表的映射。 -
通用构建: 实现了
BuildQuery、BuildSave、BuildDelete等核心方法。 -
简单函数: 通过
[SqlFunction]提供基础的 SQL 函数计算列支持。 -
基础缓存: 引入
ConcurrentDictionary缓存反射属性,优化高频调用性能。
-
2026年2月8日 | V2.0
版本说明:深度适配 Oracle 复杂业务,从“代码驱动”转向“特性声明驱动”,极大强化了查询灵活性。
-
新增功能与改进:
-
引入
[Query]特性 (重大更新):-
支持在 DTO 上直接通过特性声明
LIKE、IN、>、IS NULL等 SQL 操作符。 -
增强: 实现了“定义即逻辑”,彻底取代了旧版手动传入
likeFields数组的繁琐操作。
-
-
重构计算列体系
[Select]:-
原
[SqlFunction]升级为[Select],语义更契合 SQLSELECT逻辑。 -
增强: 支持 Oracle 包名函数调用(如
pkg_util.fn_get_info)及函数内参数占位符映射。
-
-
安全性深度加固:
-
通配符转义: 自动处理模糊查询中的 Oracle 特有转义字符(
ESCAPE '\'),修复通配符导致的数据逻辑 Bug。 -
安全黑名单: 在构建计算列和自定义片段时,增加
DROP、EXEC等危险指令的强制检测。
-
-
调试体验升级:
-
重写
ToDebugSql工具,针对 Oracle 环境自动完成to_date时间戳转换与布尔值转换,确保生成的调试 SQL 在数据库客户端 100% 原样执行。
-
-
代码逻辑瘦身:
-
提高了
BuildWhere片段的解析速度。
-
-
using Dapper;
using System.Collections.Concurrent;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
/// <summary>
/// IN 子句最大条数限制。某些数据库(如 Oracle)对 IN 子句的参数个数有限制。
/// </summary>
private const int MaxInClauseSize = 1000;
/// <summary>
/// 基础排除名单。这些属性通常属于分页基类,用于程序控制而非数据库物理列。
/// </summary>
public static readonly string[] BaseExcludes =
{
"pageNumber", "pageSize", "totalCount", "sortField", "sortOrder",
"Page", "PageSize", "TotalCount", "SortField", "SortOrder"
};
/// <summary>
/// 线程安全的反射属性缓存。
/// Key: DTO 类型完全限定名
/// Value: 属性信息数组
/// 用于降低高频反射带来的性能开销。初始容量 256,并发度为处理器数量 × 2。
/// </summary>
private static readonly ConcurrentDictionary<string, PropertyInfo[]> PropertyCache
= new(Environment.ProcessorCount * 2, 256);
/// <summary>
/// 表名缓存。
/// Key: DTO 类型完全限定名
/// Value: 表名
/// </summary>
private static readonly ConcurrentDictionary<string, string?> TableNameCache
= new(Environment.ProcessorCount * 2, 128);
/// <summary>
/// 清空所有缓存。在动态修改类定义或特性时调用此方法。
/// </summary>
public static void ClearCaches()
{
PropertyCache.Clear();
TableNameCache.Clear();
}
/// <summary>
/// SQL 构建结果。
/// </summary>
/// <param name="Sql">参数化 SQL 语句。</param>
/// <param name="Params">Dapper 动态参数对象。</param>
/// <param name="DebugSql">将参数值还原后的可调试 SQL 语句。</param>
public record SqlInfo(string Sql, DynamicParameters Params, string DebugSql);
/// <summary>
/// 自动根据 DTO 属性生成 SELECT 字段部分。
/// 支持物理列和 [Select] 特性定义的虚拟列(由数据库函数生成)。
/// </summary>
/// <typeparam name="T">DTO 实体类型。</typeparam>
/// <param name="sbSql">StringBuilder 实例,用于追加生成的 SELECT 字段列表。</param>
/// <param name="tableAlias">表别名(可选,如 "t"),用于生成带前缀的列名。</param>
/// <param name="addNewLine">字段间是否换行(默认 true),用于格式化可读性的 SQL。</param>
/// <param name="excludeFields">手动指定额外排除的属性名数组(可选)。</param>
/// <exception cref="ArgumentNullException">sbSql 为 null 时抛出异常。</exception>
/// <remarks>
/// 处理优先级:
/// 1. 优先识别 [Select] 特性定义的虚拟列(数据库函数列)
/// 2. 其次处理物理列(使用 [Column] 特性或属性名)
/// 3. 忽略 [NotMapped] 标记和 BaseExcludes 基础排除列表中的属性
///
/// [Select] 特性中的虚拟列用于 SELECT 构建,INSERT/UPDATE/DELETE 操作时被忽略。
/// </remarks>
public static void BuildSelect<T>(
StringBuilder sbSql,
string tableAlias = "",
bool addNewLine = true,
string[]? excludeFields = null)
{
if (sbSql == null)
throw new ArgumentNullException(nameof(sbSql), "StringBuilder 实例不能为 null");
string aliasPrefix = string.IsNullOrWhiteSpace(tableAlias) ? "" : $"{tableAlias}.";
var props = GetFilteredProperties<T>(excludeFields);
var fieldList = props.Select(prop =>
{
// 1. 优先识别 [Select] 特性生成虚拟列(函数列)
string? selectSql = GetDynamicAttributeValue(prop, "SelectAttribute", "SelectSql");
if (!string.IsNullOrEmpty(selectSql))
{
ValidateSelectFunction(selectSql, prop.Name);
return $"{selectSql} AS {prop.Name}";
}
// 2. 处理物理列
var colAttr = prop.GetCustomAttribute<ColumnAttribute>();
string columnName = colAttr?.Name ?? prop.Name;
return $"{aliasPrefix}{columnName} AS {prop.Name}";
}).ToList();
string separator = addNewLine ? ",\n " : ", ";
sbSql.Append(string.Join(separator, fieldList));
}
/// <summary>
/// 构建参数化 WHERE 子句片段。
/// 支持 [Query] 特性自定义操作符和条件,同时向后兼容传统的字段数组参数(likeFields、inFields)。
/// </summary>
/// <typeparam name="T">参数 DTO 类型。</typeparam>
/// <param name="paramDto">包含查询条件的 DTO 实例。若为 null 返回空 SQL。</param>
/// <param name="tableAlias">表别名(如 "t"),用于生成带前缀的列名。</param>
/// <param name="likeFields">需执行模糊查询的属性名数组(向后兼容,优先级低于 [Query] 特性)。</param>
/// <param name="inFields">需执行 IN 查询的属性名数组(向后兼容,优先级低于 [Query] 特性)。</param>
/// <param name="excludeFields">需排除构建条件的属性名数组。</param>
/// <returns>返回包含 WHERE SQL、Dapper 参数对象及调试 SQL 的 SqlInfo 对象。</returns>
/// <exception cref="ArgumentException">IN 查询条数超过限制或其他参数验证失败时抛出异常。</exception>
/// <remarks>
/// 处理优先级(从高到低):
/// 1. [Query] 特性(新特性,最高优先级,仅用于 WHERE 子句)
/// 2. 传统的 likeFields 和 inFields 参数(向后兼容)
/// 3. 自动类型推断(集合类型默认为 IN,其他类型默认为 =)
/// 4. 默认相等匹配(=)
///
/// [Query] 特性仅在 BuildWhere 方法中使用,INSERT/UPDATE/DELETE 操作时被忽略。
/// 支持的操作符:=, !=, <>, <, >, <=, >=, LIKE, IN, IS NULL, IS NOT NULL
/// 对于 IN 操作符,支持集合、数组、逗号分隔字符串等多种格式。
/// </remarks>
public static SqlInfo BuildWhere<T>(
T? paramDto,
string tableAlias = "",
string[]? likeFields = null,
string[]? inFields = null,
string[]? excludeFields = null)
{
if (paramDto == null)
return new SqlInfo(string.Empty, new DynamicParameters(), string.Empty);
var dynamicParams = new DynamicParameters();
var sbWhere = new StringBuilder();
string aliasPrefix = string.IsNullOrWhiteSpace(tableAlias) ? "" : $"{tableAlias}.";
var props = GetFilteredProperties<T>(excludeFields);
foreach (var prop in props)
{
var val = prop.GetValue(paramDto);
// 优先检查 [Query] 特性
var queryAttr = prop.GetCustomAttribute<QueryAttribute>();
if (queryAttr != null)
{
// [Query] 特性存在,使用其定义的操作符和列名
string columnName = queryAttr.ColumnName ?? prop.Name;
string operand = (queryAttr.Operand ?? "=").ToUpper();
// 检查值有效性(对于 IS NULL / IS NOT NULL 操作符跳过值检查)
if (operand != "IS NULL" && operand != "IS NOT NULL" && !IsEffectiveValue(val, prop.PropertyType))
continue;
BuildConditionWithOperand(sbWhere, dynamicParams, aliasPrefix, columnName, prop.Name, operand, val);
continue;
}
// [Query] 特性不存在,进行值有效性检查
if (!IsEffectiveValue(val, prop.PropertyType))
continue;
var attr = prop.GetCustomAttribute<ColumnAttribute>();
string columnName2 = attr?.Name ?? prop.Name;
// 处理传统的参数数组方式(向后兼容)
if (inFields != null && inFields.Contains(prop.Name))
{
BuildInCondition(sbWhere, dynamicParams, aliasPrefix, columnName2, prop.Name, val);
}
else if (likeFields != null && likeFields.Contains(prop.Name))
{
BuildLikeCondition(sbWhere, dynamicParams, aliasPrefix, columnName2, prop.Name, val);
}
else if (val is System.Collections.IEnumerable enumerable && !(val is string))
{
BuildInCondition(sbWhere, dynamicParams, aliasPrefix, columnName2, prop.Name, val);
}
else
{
sbWhere.Append($" AND {aliasPrefix}{columnName2} = :{prop.Name}");
dynamicParams.Add(prop.Name, val);
}
}
string whereSql = sbWhere.ToString();
return new SqlInfo(whereSql, dynamicParams, ToDebugSql(whereSql, dynamicParams));
}
/// <summary>
/// 构建完整的单表查询 SQL 语句(包含 SELECT、FROM、WHERE)。
/// </summary>
/// <typeparam name="T">DTO 类型。</typeparam>
/// <param name="paramDto">包含查询条件的 DTO 实例。若为 null 则仅返回 SELECT 和 FROM 部分。</param>
/// <param name="tableName">物理表名(可选,为 null 时从 [Table] 特性自动获取)。</param>
/// <param name="tableAlias">表别名(可选,如 "t")。</param>
/// <param name="likeFields">需执行模糊查询的属性名数组(向后兼容)。</param>
/// <param name="inFields">需执行 IN 查询的属性名数组(向后兼容)。</param>
/// <param name="excludeFields">需排除的属性名数组。</param>
/// <param name="customWhere">手动补充的 SQL WHERE 片段(如 "AND STATUS != 9"),必须以 AND 或 OR 开头。</param>
/// <returns>返回完整的参数化 SELECT...FROM...WHERE SQL 结果。</returns>
/// <exception cref="InvalidOperationException">无法自动获取表名或参数验证失败时抛出异常。</exception>
/// <exception cref="ArgumentException">customWhere 参数格式不正确时抛出异常。</exception>
public static SqlInfo BuildQuery<T>(
T? paramDto,
string? tableName = null,
string tableAlias = "",
string[]? likeFields = null,
string[]? inFields = null,
string[]? excludeFields = null,
string? customWhere = null)
{
tableName = GetTableName<T>(tableName);
if (string.IsNullOrWhiteSpace(tableName))
throw new InvalidOperationException(
$"无法获取表名。请在 DTO 类型 '{typeof(T).Name}' 上标注 [Table(\"table_name\")] 特性,或显式传入 tableName 参数。");
var sbSql = new StringBuilder("SELECT ");
BuildSelect<T>(sbSql, tableAlias);
sbSql.Append($"\nFROM {tableName}");
if (!string.IsNullOrWhiteSpace(tableAlias))
sbSql.Append($" {tableAlias}");
sbSql.Append("\nWHERE 1=1 ");
var whereRes = BuildWhere(paramDto, tableAlias, likeFields, inFields, excludeFields);
string fullSql = sbSql.ToString() + whereRes.Sql;
string fullDebugSql = sbSql.ToString() + whereRes.DebugSql;
if (!string.IsNullOrWhiteSpace(customWhere))
{
ValidateCustomWhere(customWhere);
fullSql += "\n" + customWhere.Trim();
fullDebugSql += "\n" + customWhere.Trim();
}
return new SqlInfo(fullSql, whereRes.Params, fullDebugSql);
}
/// <summary>
/// 构建计数查询 SQL(用于获取总记录数)。
/// 支持 [Query] 特性的自定义条件,忽略 [Select] 特性。
/// </summary>
/// <typeparam name="T">查询参数 DTO 类型。</typeparam>
/// <param name="paramDto">包含查询条件的 DTO 实例。若为 null 则返回无条件的计数语句。</param>
/// <param name="tableName">目标表名(可选,为 null 时从 [Table] 特性自动获取)。</param>
/// <param name="tableAlias">表别名(可选,如 "t")。</param>
/// <param name="likeFields">需执行模糊查询的字段名数组(向后兼容)。</param>
/// <param name="excludeFields">需排除的字段名数组。</param>
/// <param name="customWhere">自定义 WHERE 条件片段(如 "AND CREATE_TIME > SYSDATE-30"),必须以 AND 或 OR 开头。</param>
/// <param name="countExpression">自定义计数表达式(可选,默认 "COUNT(1)")。</param>
/// <returns>返回包含参数化 SELECT COUNT...FROM...WHERE SQL 的 SqlInfo 对象。</returns>
/// <exception cref="InvalidOperationException">无法获取表名或参数验证失败时抛出异常。</exception>
public static SqlInfo BuildCountQuery<T>(
T? paramDto,
string? tableName = null,
string tableAlias = "",
string[]? likeFields = null,
string[]? excludeFields = null,
string? customWhere = null,
string countExpression = "COUNT(1)")
{
tableName = GetTableName<T>(tableName);
if (string.IsNullOrWhiteSpace(tableName))
throw new InvalidOperationException(
$"无法获取表名。请在 DTO 类型 '{typeof(T).Name}' 上标注 [Table(\"table_name\")] 特性,或显式传入 tableName 参数。");
if (string.IsNullOrWhiteSpace(countExpression))
countExpression = "COUNT(1)";
var sbSql = new StringBuilder($"SELECT {countExpression}\nFROM {tableName}");
if (!string.IsNullOrWhiteSpace(tableAlias))
sbSql.Append($" {tableAlias}");
sbSql.Append("\nWHERE 1=1");
var whereRes = BuildWhere(paramDto, tableAlias, likeFields, excludeFields: excludeFields);
string fullSql = sbSql.ToString() + whereRes.Sql;
string fullDebugSql = sbSql.ToString() + whereRes.DebugSql;
if (!string.IsNullOrWhiteSpace(customWhere))
{
ValidateCustomWhere(customWhere);
fullSql += "\n" + customWhere.Trim();
fullDebugSql += "\n" + customWhere.Trim();
}
return new SqlInfo(fullSql, whereRes.Params, fullDebugSql);
}
/// <summary>
/// 构建获取 Oracle 序列值的查询 SQL。
/// </summary>
/// <param name="sequenceName">序列名称(如 "SEQ_USER")。不能为 null 或空字符串。</param>
/// <returns>返回包含 SELECT...NEXTVAL FROM DUAL SQL 的 SqlInfo 对象。</returns>
/// <exception cref="ArgumentException">序列名为空或空字符串时抛出异常。</exception>
public static SqlInfo BuildSequenceQuery(string sequenceName)
{
if (string.IsNullOrWhiteSpace(sequenceName))
throw new ArgumentException("序列名不能为空或空字符串", nameof(sequenceName));
string sql = $"SELECT {sequenceName}.NEXTVAL FROM DUAL";
var parameters = new DynamicParameters();
string debugSql = $"-- 获取 Oracle 序列值\n{sql};";
return new SqlInfo(sql, parameters, debugSql);
}
/// <summary>
/// 构建保存(新增或更新)SQL 语句及参数。
/// INSERT 和 UPDATE 操作均忽略 [Query] 和 [Select] 特性,仅处理物理列。
/// </summary>
/// <typeparam name="T">模型类型。</typeparam>
/// <param name="model">待保存的数据对象。不能为 null。</param>
/// <param name="saveType">保存类型:0 表示 INSERT(新增),1 表示 UPDATE(更新)。</param>
/// <param name="tableName">目标物理表名(可选,为 null 时从 [Table] 特性自动获取)。</param>
/// <param name="pkFields">主键属性名数组(用于 WHERE 条件和主键识别)。</param>
/// <returns>返回包含参数化 SQL 和 Dapper 参数的 SqlInfo 对象。</returns>
/// <exception cref="ArgumentNullException">model 为 null 时抛出异常。</exception>
/// <exception cref="InvalidOperationException">无法获取表名或未指定有效主键时抛出异常。</exception>
/// <exception cref="ArgumentException">saveType 无效(不是 0 或 1)或参数验证失败时抛出异常。</exception>
public static SqlInfo BuildSave<T>(
T model,
int saveType,
string? tableName = null,
string[]? pkFields = null)
{
if (model == null)
throw new ArgumentNullException(nameof(model), "模型对象不能为 null");
tableName = GetTableName<T>(tableName);
if (string.IsNullOrWhiteSpace(tableName))
throw new InvalidOperationException(
$"无法获取表名。请在 DTO 类型 '{typeof(T).Name}' 上标注 [Table(\"table_name\")] 特性,或显式传入 tableName 参数。");
pkFields ??= GetPrimaryKeyFields<T>();
if (pkFields == null || pkFields.Length == 0)
throw new InvalidOperationException(
$"未指定主键字段。请通过 pkFields 参数传入,或在属性上标注 [Key] 特性。");
var props = GetFilteredProperties<T>();
var pkList = pkFields.Select(x => x.ToLower()).ToList();
var dynamicParams = new DynamicParameters();
string sql;
if (saveType == 0)
{
sql = BuildInsertSql(model, props, tableName, pkList, dynamicParams);
}
else if (saveType == 1)
{
sql = BuildUpdateSql(model, props, tableName, pkList, dynamicParams);
}
else
{
throw new ArgumentException(
$"无效的 saveType 参数:{saveType}。允许的值为 0(INSERT)或 1(UPDATE)。",
nameof(saveType));
}
return new SqlInfo(sql, dynamicParams, ToDebugSql(sql, dynamicParams));
}
/// <summary>
/// 构建删除 SQL 语句及参数。
/// 支持单条记录删除(主键值唯一)或基于主键的批量删除(多个主键值)。
/// </summary>
/// <param name="tableName">目标物理表名。不能为 null 或空字符串。</param>
/// <param name="pkField">主键属性名(如 "poId")。不能为 null 或空字符串。</param>
/// <param name="pkValues">主键值列表。单个值生成 = 条件,多个值生成 IN 条件。不能为 null 或空列表。</param>
/// <returns>返回包含参数化 DELETE SQL 的 SqlInfo 对象。</returns>
/// <exception cref="ArgumentException">表名、主键字段名或主键值列表为空时抛出异常。</exception>
public static SqlInfo BuildDelete(
string tableName,
string pkField,
List<string> pkValues)
{
tableName = GetTableName<T>(tableName);
if (string.IsNullOrWhiteSpace(tableName))
throw new InvalidOperationException(
$"无法获取表名。请在 DTO 类型 '{typeof(T).Name}' 上标注 [Table(\"table_name\")] 特性,或显式传入 tableName 参数。");
if (string.IsNullOrWhiteSpace(pkField))
throw new ArgumentException("主键字段名不能为空或空字符串", nameof(pkField));
if (pkValues == null || pkValues.Count == 0)
throw new ArgumentException("主键值列表不能为空", nameof(pkValues));
var dynamicParams = new DynamicParameters();
string whereCondition;
if (pkValues.Count == 1)
{
whereCondition = $"{pkField} = :pkValue";
dynamicParams.Add("pkValue", pkValues[0]);
}
else
{
var paramNames = new List<string>();
for (int i = 0; i < pkValues.Count; i++)
{
string pName = $"pkVal{i}";
paramNames.Add($":{pName}");
dynamicParams.Add(pName, pkValues[i]);
}
whereCondition = $"{pkField} IN ({string.Join(", ", paramNames)})";
}
string sql = $"DELETE FROM {tableName} WHERE {whereCondition}";
return new SqlInfo(sql, dynamicParams, ToDebugSql(sql, dynamicParams));
}
/// <summary>
/// 获取调试 SQL。将参数占位符替换为实际值,用于开发调试和日志记录。
/// </summary>
/// <param name="sql">参数化 SQL 语句。</param>
/// <param name="parameters">Dapper 动态参数对象。若为 null 则直接返回 sql。</param>
/// <returns>返回参数值被还原的可调试 SQL 字符串。</returns>
public static string GetDebugSql(string sql, DynamicParameters? parameters)
{
return ToDebugSql(sql, parameters);
}
/// <summary>
/// 获取或提取表名。优先使用显式传入的 tableName,其次查找 [Table] 特性。
/// </summary>
/// <typeparam name="T">DTO 类型。</typeparam>
/// <param name="explicitTableName">显式传入的表名。若不为空则直接使用。</param>
/// <returns>获取到的表名。如果无法获取(既无参数也无特性)则返回 null。</returns>
private static string? GetTableName<T>(string? explicitTableName)
{
if (!string.IsNullOrWhiteSpace(explicitTableName))
return explicitTableName;
var typeFullName = typeof(T).FullName;
if (typeFullName == null)
return null;
if (TableNameCache.TryGetValue(typeFullName, out var cachedName))
return cachedName;
var tableAttr = typeof(T).GetCustomAttribute<TableAttribute>();
var result = tableAttr?.Name;
TableNameCache.TryAdd(typeFullName, result);
return result;
}
/// <summary>
/// 尝试从 [Key] 特性自动获取主键字段名数组。
/// </summary>
/// <typeparam name="T">DTO 类型。</typeparam>
/// <returns>主键字段名数组。如果没有找到则返回空数组。</returns>
private static string[] GetPrimaryKeyFields<T>()
{
var props = GetFilteredProperties<T>();
var keyFields = props
.Where(p => p.GetCustomAttribute<KeyAttribute>() != null)
.Select(p => p.Name)
.ToArray();
return keyFields;
}
/// <summary>
/// 检查值是否有效(应该参与数据库操作)。
/// </summary>
/// <param name="value">要检查的值。</param>
/// <param name="propertyType">属性的 CLR 类型。</param>
/// <returns>若值有效则返回 true;否则返回 false。</returns>
/// <remarks>
/// 判定规则(值为无效的情况):
/// - 值为 null
/// - DateTime/DateTimeOffset 类型(时间戳通常不作为查询条件)
/// - 空字符串或仅包含空白字符的字符串
/// - 空 GUID(Guid.Empty)
/// - 空集合(IEnumerable 但无元素)
/// </remarks>
private static bool IsEffectiveValue(object? value, Type propertyType)
{
if (value == null)
return false;
var dateTimeTypes = new[]
{
typeof(DateTime), typeof(DateTime?),
typeof(DateTimeOffset), typeof(DateTimeOffset?)
};
if (dateTimeTypes.Contains(propertyType))
return false;
if (propertyType == typeof(string))
return !string.IsNullOrWhiteSpace(value.ToString());
if (propertyType == typeof(Guid) || propertyType == typeof(Guid?))
return !Guid.Empty.Equals(value);
if (value is System.Collections.IEnumerable enumerable && !(value is string))
{
try
{
return enumerable.GetEnumerator().MoveNext();
}
catch
{
return false;
}
}
return true;
}
/// <summary>
/// 根据指定的操作符构建 WHERE 条件。
/// 用于处理 [Query] 特性定义的自定义条件。
/// </summary>
/// <param name="sbWhere">WHERE 子句 StringBuilder,用于追加条件。</param>
/// <param name="dynamicParams">动态参数对象,用于添加参数值。</param>
/// <param name="aliasPrefix">表别名前缀(如 "t.")。</param>
/// <param name="columnName">数据库列名。</param>
/// <param name="paramName">参数名(用于占位符)。</param>
/// <param name="operand">SQL 操作符,支持:=, <>, !=, <, >, <=, >=, LIKE, IN, IS NULL, IS NOT NULL。</param>
/// <param name="value">参数值。对于 IS NULL/IS NOT NULL 可为 null。</param>
private static void BuildConditionWithOperand(
StringBuilder sbWhere,
DynamicParameters dynamicParams,
string aliasPrefix,
string columnName,
string paramName,
string operand,
object? value)
{
operand = operand.ToUpper().Trim();
// 处理 IS NULL / IS NOT NULL
if (operand == "IS NULL" || operand == "IS NOT NULL")
{
sbWhere.Append($" AND {aliasPrefix}{columnName} {operand}");
return;
}
// 处理 IN 操作符
if (operand == "IN")
{
if (value == null)
throw new ArgumentException($"字段 '{paramName}' 的 IN 操作符值不能为 null");
List<string> items = new List<string>();
if (value is System.Collections.IEnumerable enumerable && !(value is string))
{
foreach (var item in enumerable)
{
if (item != null)
items.Add(item.ToString() ?? string.Empty);
}
}
else if (value is string stringValue)
{
items = stringValue
.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim())
.Where(x => !string.IsNullOrEmpty(x))
.ToList();
}
else
{
items.Add(value.ToString() ?? string.Empty);
}
if (items.Count == 0)
throw new ArgumentException($"字段 '{paramName}' 的 IN 查询条件为空");
if (items.Count > MaxInClauseSize)
throw new ArgumentException(
$"字段 '{paramName}' 的 IN 查询条件数量 {items.Count} 超过限制 {MaxInClauseSize}");
sbWhere.Append($" AND {aliasPrefix}{columnName} IN :{paramName}");
dynamicParams.Add(paramName, items);
return;
}
// 处理 LIKE 操作符
if (operand == "LIKE")
{
string stringValue = value?.ToString() ?? string.Empty;
string escapedVal = EscapeLikeWildcards(stringValue);
// Oracle 特定的 LIKE 语法
sbWhere.Append($" AND {aliasPrefix}{columnName} LIKE '%' || :{paramName} || '%' ESCAPE '\\'");
dynamicParams.Add(paramName, escapedVal);
return;
}
// 处理其他操作符(=, <>, !=, <, >, <=, >=)
sbWhere.Append($" AND {aliasPrefix}{columnName} {operand} :{paramName}");
dynamicParams.Add(paramName, value);
}
/// <summary>
/// 构建 IN 条件语句。
/// 支持集合、数组、逗号或分号分隔的字符串等多种格式。
/// </summary>
private static void BuildInCondition(
StringBuilder sbWhere,
DynamicParameters dynamicParams,
string aliasPrefix,
string columnName,
string paramName,
object value)
{
List<string>? items = null;
if (value is System.Collections.IEnumerable enumerable && !(value is string))
{
items = new List<string>();
foreach (var item in enumerable)
{
if (item != null)
items.Add(item.ToString() ?? string.Empty);
}
}
else if (value is string stringValue)
{
items = stringValue
.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim())
.Where(x => !string.IsNullOrEmpty(x))
.ToList();
}
else
{
items = new List<string> { value.ToString() ?? string.Empty };
}
if (items.Count == 0)
throw new ArgumentException($"字段 '{paramName}' 的 IN 查询条件为空,请检查输入值。");
if (items.Count > MaxInClauseSize)
throw new ArgumentException(
$"字段 '{paramName}' 的 IN 查询条件数量 {items.Count} 超过限制 {MaxInClauseSize}。请分批处理。");
sbWhere.Append($" AND {aliasPrefix}{columnName} IN :{paramName}");
dynamicParams.Add(paramName, items);
}
/// <summary>
/// 构建 LIKE 条件语句(带特殊字符转义)。
/// </summary>
private static void BuildLikeCondition(
StringBuilder sbWhere,
DynamicParameters dynamicParams,
string aliasPrefix,
string columnName,
string paramName,
object value)
{
string stringValue = value.ToString() ?? string.Empty;
string escapedVal = EscapeLikeWildcards(stringValue);
// Oracle 特定的 LIKE 语法
sbWhere.Append($" AND {aliasPrefix}{columnName} LIKE '%' || :{paramName} || '%' ESCAPE '\\'");
dynamicParams.Add(paramName, escapedVal);
}
/// <summary>
/// 构建 INSERT SQL 语句。
/// 忽略 [Query] 和 [Select] 特性,仅处理物理列。
/// </summary>
private static string BuildInsertSql<T>(
T model,
IEnumerable<PropertyInfo> props,
string tableName,
List<string> pkList,
DynamicParameters dynamicParams)
{
var sbCols = new StringBuilder();
var sbVals = new StringBuilder();
foreach (var prop in props)
{
// 忽略 [Select] 特性列
if (HasAttribute(prop, "SelectAttribute"))
continue;
// 忽略 [Query] 特性列
if (HasAttribute(prop, "QueryAttribute"))
continue;
var val = prop.GetValue(model);
bool isPk = pkList.Contains(prop.Name.ToLower());
if (!isPk && !IsEffectiveValue(val, prop.PropertyType))
continue;
var attr = prop.GetCustomAttribute<ColumnAttribute>();
string colName = attr?.Name ?? prop.Name;
// 特殊处理:Oracle 序列字符串直接拼接
if (isPk && val is string seqStr && seqStr.ToUpper().EndsWith(".NEXTVAL"))
{
sbCols.Append($"{colName}, ");
sbVals.Append($"{seqStr}, ");
continue;
}
sbCols.Append($"{colName}, ");
sbVals.Append($":{prop.Name}, ");
dynamicParams.Add(prop.Name, val);
}
if (sbCols.Length == 0)
throw new InvalidOperationException(
$"新增操作失败:没有可插入的字段值。请检查模型中是否有有效的字段数据。");
string columnList = sbCols.ToString().TrimEnd(' ', ',');
string valueList = sbVals.ToString().TrimEnd(' ', ',');
return $"INSERT INTO {tableName} ({columnList})\nVALUES ({valueList})";
}
/// <summary>
/// 构建 UPDATE SQL 语句。
/// 忽略 [Query] 和 [Select] 特性,仅处理物理列。
/// </summary>
private static string BuildUpdateSql<T>(
T model,
IEnumerable<PropertyInfo> props,
string tableName,
List<string> pkList,
DynamicParameters dynamicParams)
{
var sbSets = new StringBuilder();
var sbWheres = new StringBuilder();
foreach (var prop in props)
{
// 忽略 [Select] 特性列
if (HasAttribute(prop, "SelectAttribute"))
continue;
// 忽略 [Query] 特性列
if (HasAttribute(prop, "QueryAttribute"))
continue;
var val = prop.GetValue(model);
var attr = prop.GetCustomAttribute<ColumnAttribute>();
string colName = attr?.Name ?? prop.Name;
bool isPk = pkList.Contains(prop.Name.ToLower());
if (isPk)
{
if (!IsEffectiveValue(val, prop.PropertyType))
throw new InvalidOperationException(
$"更新操作失败:主键字段 '{prop.Name}' 的值无效或为空。");
if (sbWheres.Length > 0)
sbWheres.Append(" AND ");
sbWheres.Append($"{colName} = :{prop.Name}");
dynamicParams.Add(prop.Name, val);
}
else
{
if (!IsEffectiveValue(val, prop.PropertyType))
continue;
sbSets.Append($"{colName} = :{prop.Name}, ");
dynamicParams.Add(prop.Name, val);
}
}
if (sbSets.Length == 0)
throw new InvalidOperationException(
"更新操作失败:没有可更新的有效字段值。请检查模型中的非主键字段数据。");
if (sbWheres.Length == 0)
throw new InvalidOperationException(
"更新操作失败:未指定主键字段条件。请确保模型中包含有效的主键值。");
string setList = sbSets.ToString().TrimEnd(' ', ',');
string whereList = sbWheres.ToString();
return $"UPDATE {tableName} SET {setList}\nWHERE {whereList}";
}
/// <summary>
/// 验证自定义 WHERE 条件的合法性。
/// 检查条件格式和危险关键字。
/// </summary>
private static void ValidateCustomWhere(string customWhere)
{
if (!Regex.IsMatch(customWhere.Trim(), @"^(AND|OR)\s", RegexOptions.IgnoreCase))
throw new ArgumentException(
$"自定义 WHERE 条件必须以 AND 或 OR 开头。当前值:'{customWhere.Trim()}'");
var dangerousKeywords = new[] { "DROP", "DELETE", "TRUNCATE", "CREATE", "ALTER", "EXEC", "EXECUTE" };
foreach (var keyword in dangerousKeywords)
{
if (customWhere.Contains(keyword, StringComparison.OrdinalIgnoreCase))
throw new ArgumentException(
$"自定义 WHERE 条件包含不允许的关键字 '{keyword}',以防止 SQL 注入。");
}
}
/// <summary>
/// 验证 [Select] 特性函数的合法性。
/// 仅检查危险操作(如数据修改操作),允许参数占位符和复杂函数调用。
/// </summary>
private static void ValidateSelectFunction(string selectSql, string propertyName)
{
if (!selectSql.Contains("(") || !selectSql.Contains(")"))
throw new InvalidOperationException(
$"属性 '{propertyName}' 的 Select 特性格式无效。必须包含函数调用 (),如:'UPPER(column_name)' 或 'fun_get_name_by_code(:orgId, column_name)'。当前值:'{selectSql}'");
var dangerousPatterns = new[]
{
"DROP", "DELETE", "TRUNCATE", "INSERT", "UPDATE", "CREATE", "ALTER", "EXEC", "EXECUTE"
};
foreach (var pattern in dangerousPatterns)
{
if (Regex.IsMatch(selectSql, $@"\b{pattern}\b", RegexOptions.IgnoreCase))
throw new InvalidOperationException(
$"属性 '{propertyName}' 的 Select 特性包含不允许的关键字 '{pattern}'。当前值:'{selectSql}'");
}
int openCount = selectSql.Count(c => c == '(');
int closeCount = selectSql.Count(c => c == ')');
if (openCount != closeCount)
throw new InvalidOperationException(
$"属性 '{propertyName}' 的 Select 特性括号不匹配。开放括号数:{openCount},关闭括号数:{closeCount}。当前值:'{selectSql}'");
}
/// <summary>
/// 转义 LIKE 查询中的特殊通配符字符。
/// 用于防止特殊字符被解释为通配符。
/// </summary>
private static string EscapeLikeWildcards(string value)
{
if (string.IsNullOrEmpty(value))
return value;
return value
.Replace("\\", "\\\\") // 先转义反斜杠
.Replace("%", "\\%") // 再转义百分号
.Replace("_", "\\_"); // 最后转义下划线
}
/// <summary>
/// Dapper 参数还原工具。将 SQL 中的参数占位符替换为实际值的字符串表示。
/// 用于生成可调试的完整 SQL 语句。
/// </summary>
private static string ToDebugSql(string sql, DynamicParameters? parameters)
{
if (parameters == null || string.IsNullOrEmpty(sql))
return sql;
string debugSql = sql;
var paramNames = parameters.ParameterNames.OrderByDescending(x => x.Length);
foreach (var name in paramNames)
{
try
{
var pVal = parameters.Get<object>(name);
string formattedVal = "NULL";
if (pVal != null)
formattedVal = FormatParameterValue(pVal);
debugSql = Regex.Replace(
debugSql,
Regex.Escape(":" + name) + @"\b",
formattedVal.Replace("$", "$$"),
RegexOptions.IgnoreCase
);
}
catch
{
// 参数转换失败时,保留原始占位符
}
}
return debugSql;
}
/// <summary>
/// 格式化参数值为可用于 SQL 调试的字符串表示。
/// </summary>
private static string FormatParameterValue(object value)
{
if (value == null)
return "NULL";
return value switch
{
string str => $"'{str.Replace("'", "''")}'",
DateTime dt => $"to_date('{dt:yyyy-MM-dd HH:mm:ss}', 'yyyy-mm-dd hh24:mi:ss')",
DateTimeOffset dto => $"to_date('{dto:yyyy-MM-dd HH:mm:ss}', 'yyyy-mm-dd hh24:mi:ss')",
bool b => b ? "1" : "0",
System.Collections.IEnumerable enumerable when !(value is string) =>
FormatCollectionValue(enumerable),
_ => value.ToString() ?? "NULL"
};
}
/// <summary>
/// 格式化集合值为 Oracle SQL 列表形式。
/// </summary>
private static string FormatCollectionValue(System.Collections.IEnumerable enumerable)
{
var items = new List<string>();
try
{
foreach (var item in enumerable)
{
if (item is string sItem)
items.Add($"'{sItem.Replace("'", "''")}'");
else if (item != null)
items.Add(item.ToString() ?? "NULL");
else
items.Add("NULL");
}
}
catch
{
return "NULL";
}
return items.Count > 0 ? string.Join(", ", items) : "NULL";
}
/// <summary>
/// 获取经筛选后的属性集合。
/// 排除 BaseExcludes、[NotMapped] 特性及指定的额外排除字段。
/// </summary>
private static IEnumerable<PropertyInfo> GetFilteredProperties<T>(string[]? extraExcludes = null)
{
var typeFullName = typeof(T).FullName;
if (typeFullName == null)
return Enumerable.Empty<PropertyInfo>();
var allProps = PropertyCache.GetOrAdd(typeFullName, _ =>
{
var type = typeof(T);
return type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
});
return allProps.Where(p =>
!BaseExcludes.Contains(p.Name) &&
(extraExcludes == null || !extraExcludes.Contains(p.Name)) &&
p.GetCustomAttribute<NotMappedAttribute>() == null);
}
/// <summary>
/// 动态获取指定特性的属性值。
/// 解决同名特性在不同命名空间下的冲突问题。
/// </summary>
private static string? GetDynamicAttributeValue(PropertyInfo prop, string attrName, string propertyName)
{
try
{
var attr = prop.GetCustomAttributes()
.FirstOrDefault(a => a.GetType().Name == attrName);
if (attr == null)
return null;
var propertyInfo = attr.GetType().GetProperty(
propertyName,
BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase
);
return propertyInfo?.GetValue(attr)?.ToString();
}
catch
{
return null;
}
}
/// <summary>
/// 判断属性是否标注了指定的特性。
/// 基于特性类名字符串匹配,不依赖完全限定名。
/// </summary>
private static bool HasAttribute(PropertyInfo prop, string attrName)
{
return prop.GetCustomAttributes()
.Any(a => a.GetType().Name == attrName);
}
/// <summary>
/// SELECT 虚拟列特性。用于标记属性为 SELECT 字段中的计算列(由数据库函数生成)。
/// </summary>
/// <remarks>
/// <para>特点:</para>
/// <list type="bullet">
/// <item><description>仅在 BuildSelect 方法中使用,生成 SELECT 列表</description></item>
/// <item><description>在 INSERT、UPDATE、DELETE 操作中被忽略</description></item>
/// <item><description>支持参数占位符如 :paramName,支持复杂函数调用</description></item>
/// <item><description>支持嵌套函数和包名.函数的格式</description></item>
/// </list>
///
/// <para>使用示例:</para>
/// <code>
/// // 简单函数
/// [Select("UPPER(user_name)")]
/// public string UserNameUpper { get; set; }
///
/// // 带参数占位符的函数
/// [Select("fun_get_name_by_code('user', :orgId, AUDIT_BY)")]
/// public string AuditByName { get; set; }
///
/// // 嵌套函数
/// [Select("UPPER(SUBSTR(user_name, 1, 10))")]
/// public string UserNameShort { get; set; }
///
/// // 包名.函数格式
/// [Select("pkg_util.fn_get_user_info(:userId)")]
/// public string UserInfo { get; set; }
/// </code>
/// </remarks>
[AttributeUsage(AttributeTargets.Property)]
public sealed class SelectAttribute : Attribute
{
/// <summary>
/// 初始化 SelectAttribute 实例。
/// </summary>
/// <param name="selectSql">
/// SQL 函数调用字符串。
/// 支持:
/// - 简单函数:UPPER(column_name)
/// - 带字面量:SUBSTR(column_name, 1, 10)
/// - 带参数占位符:fun_get_name_by_code('user', :orgId, column_name)
/// - 嵌套函数:UPPER(SUBSTR(column_name, 1, 10))
/// - 包名.函数:pkg_util.fn_get_info(:param)
/// </param>
/// <exception cref="ArgumentException">selectSql 为空或空字符串时抛出异常。</exception>
public SelectAttribute(string selectSql)
{
if (string.IsNullOrWhiteSpace(selectSql))
throw new ArgumentException("Select 函数不能为空或空字符串", nameof(selectSql));
SelectSql = selectSql;
}
/// <summary>
/// 获取 SQL 函数调用字符串。
/// </summary>
public string SelectSql { get; }
}
/// <summary>
/// 查询条件特性。用于标记属性在 WHERE 子句中的自定义条件和 SQL 操作符。
/// </summary>
/// <remarks>
/// <para>特点:</para>
/// <list type="bullet">
/// <item><description>仅在 BuildWhere 方法中使用,生成 WHERE 条件</description></item>
/// <item><description>在 INSERT、UPDATE、DELETE 操作中被忽略</description></item>
/// <item><description>支持多种 SQL 操作符,包括比较、模糊、IN 等</description></item>
/// <item><description>支持自定义列名,与属性名不同的数据库列</description></item>
/// <item><description>注:若处理相等时 可采用 [Column("id")] 等同于 [Query("id")]</description></item>
/// </list>
///
/// <para>支持的操作符:</para>
/// <list type="bullet">
/// <listheader><term>操作符</term><description>说明 (示例)</description></listheader>
/// <item><term>=</term><description>相等(默认)。例:[Query("id")]</description></item>
/// <item><term>!=</term><description>不等于。例:[Query("status", "!=")]</description></item>
/// <item><term><></term><description>不等于。例:[Query("status", "<>")]</description></item>
/// <item><term><</term><description>小于。例:[Query("createTime", "<")]</description></item>
/// <item><term>></term><description>大于。例:[Query("createTime", ">")]</description></item>
/// <item><term><=</term><description>小于等于。例:[Query("endTime", "<=")]</description></item>
/// <item><term>>=</term><description>大于等于。例:[Query("startTime", ">=")]</description></item>
/// <item><term>LIKE</term><description>模糊查询。例:[Query("name", "LIKE")]</description></item>
/// <item><term>IN</term><description>包含。例:[Query("status", "IN")]</description></item>
/// <item><term>IS NULL</term><description>空值判断。例:[Query("deletedAt", "IS NULL")]</description></item>
/// <item><term>IS NOT NULL</term><description>非空值判断。例:[Query("deletedAt", "IS NOT NULL")]</description></item>
/// </list>
///
/// <para>使用示例:</para>
/// <code>
/// // 简单相等
/// [Query("user_id")]
/// public string UserId { get; set; }
/// // 生成:AND user_id = :UserId
///
/// // 比较操作符 (注意:在 C# 代码字符串中可以直接写 < 和 >)
/// [Query("CreateTime", "<=")]
/// public DateTime? Search_EndTime { get; set; }
/// // 生成:AND t.CreateTime <= :Search_EndTime
///
/// // IN 查询(支持集合)
/// [Query("Status", "IN")]
/// public List<int> StatusList { get; set; }
/// // 生成:AND Status IN (:StatusList)
/// </code>
/// </remarks>
[AttributeUsage(AttributeTargets.Property)]
public sealed class QueryAttribute : Attribute
{
/// <summary>
/// 初始化 QueryAttribute 实例。
/// </summary>
/// <param name="columnName">
/// 对应的数据库列名。
/// 若为 null 或空则使用属性名。
/// </param>
/// <param name="operand">
/// SQL 操作符。
/// 支持的值:=, !=, <>, <, >, <=, >=, LIKE, IN, IS NULL, IS NOT NULL
/// 默认为 "=" (相等)。大小写不敏感,会自动转换为大写。
/// </param>
/// <exception cref="ArgumentException">columnName 为空或空字符串时抛出异常。</exception>
public QueryAttribute(string columnName, string? operand = "=")
{
if (string.IsNullOrWhiteSpace(columnName))
throw new ArgumentException("列名不能为空或空字符串", nameof(columnName));
ColumnName = columnName;
Operand = operand ?? "=";
}
/// <summary>
/// 获取对应的数据库列名。
/// </summary>
public string ColumnName { get; }
/// <summary>
/// 获取 SQL 操作符(如 =, LIKE, IN 等)。
/// 大小写不敏感,会在使用时自动转换为大写。
/// </summary>
public string Operand { get; }
}
文章评论