简介这份资源面向需要在 C# 项目中对接 Oracle 数据库的开发者尤其适合希望摆脱手写实体类、借助 ORM 提升数据访问层效率的中级 .NET 程序员。内容围绕 OracleCodeGenerator 工具展开涵盖 Entity Framework 与 Oracle 提供程序的配合、连接字符串配置、实体类生成与设计原则以及 DbContext 使用、Code First 迁移和 Repository 模式等扩展思路可帮助读者快速搭建与 Oracle 表结构对应的模型层。压缩包为 rar 格式共 62 个文件约 2.99MB以 18 个 cs 源码文件为核心辅以 dll 依赖库、xaml 界面文件、xml 配置与少量 exe、ico 等资源构成一个可直接运行的 WPF 生成器工程。目前已有 214 人学习下载适合作为 C# 与 Oracle 交互场景下的实体类生成参考与二次开发基础。1. 从一张手工维护的实体类表说起接手一个 C# 加 Oracle 的老项目时最先让人头疼的往往不是业务逻辑而是数据访问层里那几十个手写的实体类。表结构一改字段名一换编译能过、运行才炸排查起来像开盲盒。这个项目的定位很明确围绕 C# 与 Oracle 的组合把「数据库表结构 → 实体类代码」这条链路自动化减少手抄字段带来的低级错误。它适合正在维护 Oracle 后台系统、需要频繁同步表结构与 C# 模型的开发者也适合想理解代码生成器内部怎么拼字符串、怎么读元数据的人。下面按「能干什么、怎么跑、怎么改」的顺序拆开讲。2. 项目定位与技术选型为什么是 C# 直连 Oracle 做代码生成2.1 它解决的不是 ORM 问题而是模型同步问题很多人第一反应是「有 Entity Framework 或 Dapper 了还要实体类生成器干嘛」。这个项目的价值不在运行时映射而在开发期的模型同步。Oracle 里一张业务表动辄三四十个字段类型还杂NUMBER(10)、VARCHAR2(200)、DATE、CLOB。手写实体类时decimal还是int、DateTime还是string全靠人记。生成器的做法是读 Oracle 的数据字典视图把列名、数据类型、精度、可空性一次性拉出来再按映射规则拼成 C# 类。它不替代 ORM而是给 ORM 提供准确的模型输入。技术选型上这类工具通常走两条路一是纯System.Data.OracleClient或第三方驱动直连二是通过ODP.NETOracle 官方 .NET 驱动。项目正文没有指定驱动按这个场景下合格开发最可能用的做法我会选Oracle.ManagedDataAccess因为它免装客户端、部署干净连接字符串里配好Data Source就能跑。生成端用控制台程序最省事输出.cs文件到指定目录再让主项目引用。2.2 元数据从哪来USER_TAB_COLUMNS 是主战场Oracle 的数据字典是生成器的「黑匣子」入口。最常用的是USER_TAB_COLUMNS它只返回当前 schema 下的表列信息如果要跨 schema就换ALL_TAB_COLUMNS并加OWNER条件。关键字段包括TABLE_NAME、COLUMN_NAME、DATA_TYPE、DATA_LENGTH、DATA_PRECISION、DATA_SCALE、NULLABLE、COLUMN_ID。COLUMN_ID决定字段顺序生成时按它排序实体类字段顺序就和表结构一致读代码时不容易错位。-- 读取指定表的列元数据按列顺序排列 SELECT COLUMN_NAME, DATA_TYPE, DATA_LENGTH, DATA_PRECISION, DATA_SCALE, NULLABLE, COLUMN_ID FROM USER_TAB_COLUMNS WHERE TABLE_NAME T_ORDER ORDER BY COLUMN_ID;这段查询是生成器的数据源。DATA_TYPE返回的是 Oracle 类型名比如NUMBER、VARCHAR2DATA_PRECISION和DATA_SCALE只在NUMBER上有意义用来判断该映射成int、long还是decimal。NULLABLE为Y时值类型字段要加?变成可空否则插入空值会抛异常。参数上唯一要改的是TABLE_NAME实际工具里会做成循环把目标表清单逐个查一遍。2.3 目录结构一个控制台加一个模板目录按常见做法项目结构大致如下生成器本身不复杂重点是模板和映射规则分离OracleEntityGen/ ├── Program.cs // 入口读配置、遍历表、写文件 ├── Config/ │ └── appsettings.json // 连接串、表清单、输出目录 ├── Metadata/ │ └── OracleSchemaReader.cs // 查数据字典返回列信息集合 ├── Mapping/ │ └── TypeMapper.cs // Oracle 类型 → C# 类型 ├── Template/ │ └── EntityTemplate.cs // 拼类名、属性、命名空间 └── Output/ // 生成的 .cs 文件落这里把「读元数据」「类型映射」「模板拼装」拆成三个类是为了后面改规则时不动其他部分。比如某张表的NUMBER(1)想映射成bool而不是int只改TypeMapper就行想给生成的类加[Serializable]只改模板。这种分层在生成器里很值因为生成规则会随项目演进反复调。3. 核心模块拆解从查表到落盘的完整链路3.1 连接与读取把列信息装进一个强类型集合先定义一个承载列元数据的类字段和查询结果一一对应。这样后面映射时不用再碰DataTable类型安全调试也直观。public class ColumnInfo { public string ColumnName { get; set; } public string DataType { get; set; } public int DataLength { get; set; } public int? Precision { get; set; } public int? Scale { get; set; } public bool IsNullable { get; set; } public int ColumnId { get; set; } }读取时用OracleCommand执行前面的 SQL逐行填充。注意DATA_PRECISION和DATA_SCALE在非NUMBER列上可能是DBNull取值前要判空否则Convert.ToInt32直接翻车。NULLABLE读出来是字符串Y/N转bool时用 Y判断别用Convert.ToBoolean它对N的处理和直觉不一致。public ListColumnInfo ReadColumns(string tableName) { var list new ListColumnInfo(); using var conn new OracleConnection(_connectionString); conn.Open(); using var cmd conn.CreateCommand(); cmd.CommandText SELECT COLUMN_NAME, DATA_TYPE, DATA_LENGTH, DATA_PRECISION, DATA_SCALE, NULLABLE, COLUMN_ID FROM USER_TAB_COLUMNS WHERE TABLE_NAME :t ORDER BY COLUMN_ID; cmd.Parameters.Add(:t, OracleDbType.Varchar2).Value tableName; using var reader cmd.ExecuteReader(); while (reader.Read()) { list.Add(new ColumnInfo { ColumnName reader.GetString(0), DataType reader.GetString(1), DataLength reader.GetInt32(2), Precision reader.IsDBNull(3) ? null : reader.GetInt32(3), Scale reader.IsDBNull(4) ? null : reader.GetInt32(4), IsNullable reader.GetString(5) Y, ColumnId reader.GetInt32(6) }); } return list; }参数用:t绑定而不是字符串拼接一是防注入二是 Oracle 对字面量大小写敏感绑定值能避免T_ORDER和t_order查不到的问题。连接串里User Id对应的 schema 决定了USER_TAB_COLUMNS的范围跨 schema 时记得换视图并加OWNER条件。3.2 类型映射NUMBER 的精度决定一切Oracle 到 C# 的映射里NUMBER是最容易出错的。NUMBER(1)到NUMBER(9)可以放intNUMBER(10)到NUMBER(18)用long再大或者带小数就用decimal。VARCHAR2、NVARCHAR2、CHAR统一映射stringDATE和TIMESTAMP映射DateTimeCLOB映射stringBLOB映射byte[]。这套规则写成switch最清楚。public static string MapToCSharp(ColumnInfo col) { var t col.DataType.ToUpperInvariant(); switch (t) { case NUMBER: if (col.Scale is 0) return decimal; // 有小数位 if (col.Precision is null or 18) return decimal; if (col.Precision 9) return int; return long; case VARCHAR2: case NVARCHAR2: case CHAR: case NCHAR: case CLOB: return string; case DATE: case TIMESTAMP: return DateTime; case BLOB: return byte[]; default: return string; // 未知类型兜底生成后人工确认 } }Scale大于 0 说明有小数必须decimal用double会在金额计算上丢精度这是血泪经验。Precision为空的情况出现在没指定精度的NUMBER上按decimal处理最稳。兜底返回string不是偷懒是让生成结果至少能编译未知类型在代码里一眼能看出来比生成一个错误类型导致运行时异常强。3.3 模板拼装类名、属性、可空标记一次成型模板部分本质是字符串拼装但有几个细节决定生成质量。类名默认用表名转帕斯卡命名去掉T_前缀属性名用列名转帕斯卡可空列的值类型加?。string和byte[]本身是引用类型不需要?但可以加 string.Empty初始化避免空引用。public string BuildEntity(string tableName, ListColumnInfo columns) { var className ToPascal(tableName.Replace(T_, )); var sb new StringBuilder(); sb.AppendLine(using System;); sb.AppendLine(namespace Generated.Entities); sb.AppendLine({); sb.AppendLine($ public class {className}); sb.AppendLine( {); foreach (var col in columns) { var csType TypeMapper.MapToCSharp(col); var propName ToPascal(col.ColumnName); var nullable col.IsNullable IsValueType(csType) ? ? : ; sb.AppendLine($ public {csType}{nullable} {propName} {{ get; set; }}); } sb.AppendLine( }); sb.AppendLine(}); return sb.ToString(); }ToPascal负责把ORDER_NO转成OrderNo下划线分段后首字母大写。IsValueType判断int、long、decimal、DateTime这些值类型只有它们才需要?。生成的文件按类名命名落到Output目录主项目用「添加现有项」或干脆把目录做成链接引用。这样表结构变了重跑一次生成器覆盖旧文件即可。4. 跑起来与避坑连接、命名、覆盖三件事4.1 最小可运行步骤先把Oracle.ManagedDataAccess通过 NuGet 装进控制台项目然后在appsettings.json里配连接串和表清单。连接串形如User Idyour_schema;Passwordyour_pwd;Data Sourcehost:1521/service_nameData Source的写法取决于监听配置service_name和SID别混。表清单用一个字符串数组循环调用ReadColumns和BuildEntity最后File.WriteAllText落盘。跑之前确认当前数据库用户对目标表有查询权限USER_TAB_COLUMNS只显示当前用户拥有的表权限不够会查不到列生成出来是空类。4.2 常见问题排查现象、原因、解决现象一生成的类字段全是string。原因通常是DATA_TYPE读出来带尾随空格或大小写不一致switch没命中。解决是在MapToCSharp里先Trim().ToUpperInvariant()再进switch别假设数据库返回值干净。现象二NUMBER列生成int但运行时报溢出。原因是Precision为null时被当成了小精度。解决是把Precision is null单独归到decimal分支宁可宽不可窄金额和主键尤其如此。现象三可空列生成后插入null仍报错。检查IsNullable判断是不是用了Convert.ToBoolean(N)它返回true导致可空标记反了。统一用 Y判断简单可靠。现象四重跑生成器把手工加的注释冲掉了。这是覆盖式生成的通病。解决是把手工扩展放到partial类的另一个文件里生成文件只放属性扩展文件放方法、特性和注释重跑不丢。现象五表名带小写或特殊字符类名生成非法。Oracle 默认表名大写但用双引号建的表会保留大小写。解决是在ToPascal里过滤非字母数字字符首字符是数字时前面补下划线保证生成的标识符合法。提示生成器第一次跑建议只选一张表人工核对生成结果和表结构是否一致确认映射规则无误后再批量跑避免一次性覆盖几十个文件后才发现规则错了。5. 二次开发把生成器改成团队能长期用的工具生成器跑通只是起点真正让它留在项目里的是可维护性。我一般会做三件事。第一把表清单从硬编码挪到配置文件支持按前缀过滤比如只生成T_开头的表避免把日志表、临时表也生成进来。第二给生成的类加partial关键字手工扩展放独立文件这样每次重跑不丢业务方法。第三加一个「差异检查」模式生成到内存后和磁盘上已有文件比对内容一致就跳过写入减少无意义的文件变更代码评审时 diff 也干净。// 差异检查内容相同则不落盘避免无意义覆盖 var newContent generator.BuildEntity(table, columns); var path Path.Combine(outputDir, ${className}.cs); if (File.Exists(path) File.ReadAllText(path) newContent) { Console.WriteLine($skip {className}, no change); continue; } File.WriteAllText(path, newContent);这段逻辑的价值在团队协作里很明显生成器重跑不会产生一堆「只改了时间戳」的提交评审时只看真正变化的字段。参数上outputDir建议放在主项目的Entities/Generated子目录和手工代码物理隔离谁改了生成文件一眼能看出来。再往深一层可以把类型映射规则也做成配置比如某张表的STATUS列想映射成枚举而不是int就在配置里加一条「表名 列名 → 自定义类型」的覆盖规则TypeMapper先查覆盖表再走默认逻辑。这样生成器从「一次性脚本」变成「团队约定」的载体新表接入时不用再讨论字段类型怎么定。从那以后我每次改表结构都强制先跑一遍生成器再提交生成结果和 DDL 一起进版本库谁改了什么一目了然。希望帮到你。本文还有配套的精品资源点击获取