C#调用Oracle返回REF CURSOR的存储过程,必须用OracleDbType.RefCursor+OracleDataAdapter.Fill填充DataTable,不可用ExecuteReader;类型转换需判空并按NUMBER/DATE/OBJECT差异处理。

Oracle存储过程返回REF CURSOR时,C#怎么拿到数据
直接用 OracleCommand 执行带 OUT 参数的存储过程,把参数类型设为 OracleDbType.RefCursor,然后用 OracleDataAdapter 填充 DataTable——这是最稳的路径。别试图手动读取游标流或反复调用 Fetch,Oracle .NET 驱动不支持那种低层操作。
常见错误是把 REF CURSOR 参数类型写成 OracleDbType.Cursor(旧版驱动用)或 OracleDbType.Object,结果报错 ORA-06550: PLS-00306: wrong number or types of arguments。新版本 Oracle.ManagedDataAccess 中必须用 RefCursor。
- 确保存储过程中声明的是
OUT sys_refcursor,不是自定义 ref cursor 类型 - 在 C# 中添加参数时,
Direction必须设为ParameterDirection.Output - 执行前调用
command.CommandType = CommandType.StoredProcedure,否则驱动可能忽略 OUT 参数
从DataTable转成强类型List的三种实用方式
拿到 DataTable 后,转换逻辑取决于你的 T 是否有无参构造函数、属性名是否和列名完全匹配、是否需要类型容错。别一上来就用 JsonConvert.DeserializeObject<list>>(JsonConvert.SerializeObject(dt))</list>,性能差还容易因 null 或日期格式崩掉。
推荐按优先级选:
- 如果列名和
T的 public 属性名一致(大小写敏感),用DataTable.AsEnumerable().Select(...).ToList()手动映射——控制力最强,可加空值检查 - 如果只是简单投影且字段少,用
DataTable.AsEnumerable().Cast<datarow>().Select(r => new T { Prop = r.Field<string>("Col") }).ToList()</string></datarow> - 若已引入
System.Data.DataSetExtensionsNuGet 包,可用DataTable.AsEnumerable().Select(DataRowToT).ToList(),但注意它不处理类型转换异常
别依赖 DataTable.CreateDataReader() 再用 SqlMapper.ToList<t>()</t>(Dapper),Oracle 驱动的 OracleDataReader 不兼容 Dapper 的类型绑定逻辑,会静默跳过字段或抛 InvalidCastException。
为什么不能直接用OracleCommand.ExecuteReader()读REF CURSOR
因为 ExecuteReader() 只适用于 SQL 查询语句(CommandType.Text),对存储过程中的 OUT RefCursor 参数无效。你调用后得到的 OracleDataReader 是空的,HasRows 为 false,且不会报错——这是最容易被忽略的坑。
真正有效的流程是:先 ExecuteNonQuery() 触发存储过程执行,再从 OracleCommand.Parameters["out_cursor"].Value 取出游标对象(实际是 OracleRefCursor 实例),最后把它传给 OracleDataAdapter 的 SelectCommand 属性,再调用 Fill()。
- 有人尝试把
Parameters["out_cursor"].Value强转成OracleRefCursor后调GetDataReader(),这在新版驱动中会抛NotSupportedException -
OracleRefCursor本身没有公开的枚举方法,它只是一个占位符,必须走DataAdapter路径
类型转换失败的典型场景和绕过方法
Oracle 的 NUMBER(1) 映射到 C# 可能是 decimal、int 或 bool,而 DATE 默认变成 DateTime 但精度只到秒(Oracle 存毫秒)。一旦 DataRow["col"] 是 DBNull.Value,直接调 .ToString() 或强制转型就会炸。
- 统一用
row.IsNull("col") ? null : row.Field<t>("col")</t>判断空值,别用== DBNull.Value - 对布尔字段,Oracle 没原生 bool 类型,通常用
NUMBER(1)存 0/1,映射时写row.Field<int>("is_active") == 1</int> - 日期字段若需毫秒级,改用
row.Field<datetime>("created_at").AddTicks(...)</datetime>补精度(前提是 Oracle 端用了TIMESTAMP)
最麻烦的是自定义对象类型(比如 Oracle 的 OBJECT 类型),OracleDataAdapter 填充后对应列为 OracleObject,不能直接 cast。这种必须用 OracleObject.GetTable() 或提前在存储过程中用 CAST(COLLECT(...)) 拆成标准表结构。


















