在Dapper中使用模板对象创建存储过程的动态参数
刚好之前做类似需求的时候研究过,用反射来批量处理类属性转DynamicParameters是最高效的方式,不用手动一个个写Add参数,省心又不容易出错。下面给你一步步讲怎么实现:
核心思路
利用C#反射遍历输入/输出类的公共属性,自动将属性名和对应值映射到DynamicParameters中——输入类转成存储过程的输入参数,输出类则预先在DynamicParameters中声明输出参数,执行完存储过程后再把参数值回填到输出对象里。
扩展方法实现(复用性拉满)
写一个静态扩展类,封装转换逻辑,这样任何输入/输出类都能直接用:
using Dapper; using System.Data; using System.Reflection; public static class DynamicParametersExtensions { // 将输入对象转换为存储过程输入参数 public static DynamicParameters ToInputParameters<T>(this T inputObj) where T : class { var parameters = new DynamicParameters(); if (inputObj == null) return parameters; // 遍历输入类的所有公共实例属性 foreach (var prop in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)) { // 跳过标记了忽略特性的属性(可选,后面会讲) if (prop.GetCustomAttribute<IgnoreParameterAttribute>() != null) continue; var propValue = prop.GetValue(inputObj); // 存储过程参数通常带@前缀,根据你的实际命名规则调整 parameters.Add($"@{prop.Name}", propValue); } return parameters; } // 从执行后的DynamicParameters中提取值到输出对象 public static T ToOutputObject<T>(this DynamicParameters parameters) where T : class, new() { var outputObj = new T(); foreach (var prop in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)) { var paramName = $"@{prop.Name}"; if (parameters.ParameterNames.Contains(paramName)) { var paramValue = parameters.Get<object>(paramName); // 处理可空类型的赋值 if (paramValue != DBNull.Value) prop.SetValue(outputObj, paramValue); } } return outputObj; } // 辅助方法:根据属性类型自动匹配DbType(用于输出参数声明) private static DbType GetDbType(Type propertyType) { // 基础类型映射,可根据需求扩展 return propertyType switch { Type t when t == typeof(string) || t == typeof(char) => DbType.String, Type t when t == typeof(int) || t == typeof(int?) => DbType.Int32, Type t when t == typeof(DateTime) || t == typeof(DateTime?) => DbType.DateTime, Type t when t == typeof(decimal) || t == typeof(decimal?) => DbType.Decimal, _ => DbType.Object }; } // 批量添加输出参数到DynamicParameters public static void AddOutputParameters<T>(this DynamicParameters parameters) where T : class { foreach (var prop in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)) { var dbType = GetDbType(prop.PropertyType); parameters.Add($"@{prop.Name}", dbType: dbType, direction: ParameterDirection.Output); } } } // 可选:自定义特性,标记不需要作为参数的属性 [AttributeUsage(AttributeTargets.Property)] public class IgnoreParameterAttribute : Attribute { }
实际调用示例
结合你提供的StudentInput和StudentOutput类,调用存储过程的代码会非常简洁:
using System.Data.SqlClient; // 1. 初始化输入对象 var studentInput = new StudentInput(); // 默认StudentId=1 // 2. 转换为输入参数 var parameters = studentInput.ToInputParameters(); // 3. 添加输出参数(自动匹配类型) parameters.AddOutputParameters<StudentOutput>(); // 4. 执行存储过程 using (var conn = new SqlConnection("你的数据库连接字符串")) { conn.Open(); conn.Execute("你的存储过程名称", parameters, commandType: CommandType.StoredProcedure); } // 5. 提取输出结果到StudentOutput对象 var studentOutput = parameters.ToOutputObject<StudentOutput>(); // 验证结果 Console.WriteLine($"学生姓名:{studentOutput.StudentName}"); Console.WriteLine($"喜欢的科目:{studentOutput.FavSubject}"); Console.WriteLine($"年级:{studentOutput.Grade}"); Console.WriteLine($"入学年份:{studentOutput.YearOfJoining}");
优化与注意事项
- 参数名称匹配:确保存储过程的参数名和类的属性名完全一致(SQL Server默认不区分大小写,但严格匹配能避免意外问题)。如果存储过程参数有前缀(比如
@In_StudentId),只需要修改扩展方法里的参数名拼接逻辑即可。 - 忽略特定属性:如果输入类里有不需要作为参数的属性,给它加上
[IgnoreParameter]特性,扩展方法会自动跳过。 - 可空类型处理:代码里已经处理了
DBNull.Value的情况,避免给可空类型赋值时报错。 - 类型映射扩展:如果你的输出类有更多数据类型(比如
Guid、bool),可以在GetDbType方法里添加对应的映射规则。
内容的提问来源于stack exchange,提问作者Jenny
相关产品推荐
相关产品推荐

