如何为返回结构动态变化的SQL Server存储过程生成DTO模型?
针对存储过程返回结构随表名动态变化的场景,给你几个实用的解决方案:
方案1:使用DataTable/DataSet(最直接)
直接用DataTable接收动态结果,它会自动根据返回的列构建结构,无需预定义DTO。
修改你的数据访问方法,新增一个非泛型的存储过程执行方法:
public DataTable ExecuteProcedure(string procedureName, Dictionary<string, object> parameters) { DataTable result = new DataTable(); using (SqlConnection conn = new SqlConnection("your_connection_string")) { conn.Open(); using (SqlCommand cmd = new SqlCommand(procedureName, conn)) { cmd.CommandType = CommandType.StoredProcedure; foreach (var param in parameters) { cmd.Parameters.AddWithValue(param.Key, param.Value); } using (SqlDataAdapter adapter = new SqlDataAdapter(cmd)) { adapter.Fill(result); } } } return result; }
调用示例:
public DataTable GetDynamicTableData(string tableName) { // 关键:先验证表名合法性,防止SQL注入 if (!IsValidTableName(tableName)) { throw new ArgumentException("Invalid table name"); } Dictionary<string, object> parameters = new Dictionary<string, object>(); parameters.Add("@TableName", tableName); return ExecuteProcedure("spGetDynamicTableData", parameters); } // 示例:验证表名是否在允许的列表中 private bool IsValidTableName(string tableName) { var allowedTables = new List<string> { "UserDetails", "OrderInfo", "ProductData" }; return allowedTables.Contains(tableName, StringComparer.OrdinalIgnoreCase); }
优点:实现简单,无需额外依赖,完全适配动态结构;缺点:不够面向对象,数据访问需要通过列名索引,代码可读性稍差。
方案2:使用dynamic动态类型
如果用Dapper这类ORM,可以直接返回dynamic类型的列表,无需预定义DTO。
假设你的ExecuteProcedure是基于Dapper封装的,重载一个返回动态类型的版本:
public IList<dynamic> ExecuteProcedureDynamic(string procedureName, Dictionary<string, object> parameters) { using (SqlConnection conn = new SqlConnection("your_connection_string")) { conn.Open(); return conn.Query<dynamic>(procedureName, parameters, commandType: CommandType.StoredProcedure).ToList(); } }
调用示例:
public IList<dynamic> GetDynamicTableData(string tableName) { if (!IsValidTableName(tableName)) { throw new ArgumentException("Invalid table name"); } var parameters = new Dictionary<string, object> { { "@TableName", tableName } }; return ExecuteProcedureDynamic("spGetDynamicTableData", parameters); }
使用时可以直接访问属性:
var data = GetDynamicTableData("UserDetails"); foreach (var item in data) { Console.WriteLine(item.UserId + " " + item.UserName); }
优点:语法接近强类型,使用方便;缺点:编译时无类型检查,拼写错误会导致运行时异常,IDE无智能提示。
方案3:使用ExpandoObject
ExpandoObject允许你动态添加属性,适合需要在代码中灵活操作属性的场景:
可以基于DataTable转换为ExpandoObject列表:
public IList<ExpandoObject> DataTableToExpando(DataTable table) { var expandoList = new List<ExpandoObject>(); foreach (DataRow row in table.Rows) { var expando = new ExpandoObject(); var expandoDict = (IDictionary<string, object>)expando; foreach (DataColumn col in table.Columns) { expandoDict[col.ColumnName] = row[col] == DBNull.Value ? null : row[col]; } expandoList.Add(expando); } return expandoList; }
调用时先获取DataTable再转换:
public IList<ExpandoObject> GetDynamicTableData(string tableName) { var dt = ExecuteProcedure("spGetDynamicTableData", new Dictionary<string, object> { { "@TableName", tableName } }); return DataTableToExpando(dt); }
优点:完全动态,可在运行时修改属性;缺点:性能略低于dynamic,同样无编译时类型检查。
关键注意事项:SQL注入防护
因为传入的是表名参数,直接拼接或传入未验证的表名会导致严重的SQL注入风险,必须做以下防护:
- 白名单验证:只允许访问预先定义的合法表名(如方案1中的
IsValidTableName方法); - 使用
QUOTENAME函数:在存储过程中用QUOTENAME(@TableName)包裹表名,避免注入;
存储过程示例:CREATE PROCEDURE spGetDynamicTableData @TableName NVARCHAR(128) AS BEGIN DECLARE @Sql NVARCHAR(MAX) SET @Sql = N'SELECT * FROM ' + QUOTENAME(@TableName) EXEC sp_executesql @Sql END
内容的提问来源于stack exchange,提问作者Sayan
相关产品推荐
相关产品推荐

