
简介面向C#开发者与.NET数据访问层初学者这份资源聚焦Oracle数据库实体类的自动生成与使用帮助减少手写映射代码的工作量。包内共62个文件以18个cs源码文件为核心配合9个dll依赖库、3个xaml界面文件、3个xml配置及sln、csproj等工程文件另有ico图标、pdb调试符号与缓存文件压缩包约2.99MB可直接解压运行。已有214人学习下载。资源围绕OracleCodeGenerator工具展开涵盖连接字符串配置、按表或视图批量生成实体类、属性类型与列类型对应、主键与表名注解等要点并延伸至DbContext上下文配置、Linq查询及增删改操作示例同时提及Code First迁移、Repository模式与查询优化思路便于读者快速搭建数据访问层并理解ORM映射机制。1. 从一张手写实体类说起C# 对接 Oracle 的实体层到底省了什么接手一个老系统维护任务时最耗时的往往不是业务逻辑而是数据访问层那一堆手写实体类。表有三十多个字段就要在 C# 里敲三十多个属性还要和 Oracle 的字段类型一一对上NUMBER(10)对应int还是longNUMBER(10,2)对应decimalVARCHAR2(50)对应stringDATE对应DateTime还是DateTime?。手敲一遍错一个类型运行时就是一次类型转换异常。这份资源围绕的就是「C# 操作 Oracle 时如何创建实体类」这条主线把建表、字段映射、实体生成、增删改查封装串成一套能直接落地的做法。适合正在用 C# 对接 Oracle、被手写实体类折磨过的后端开发者也适合刚接手存量项目、需要快速补齐数据访问层的新手。下面按「能干什么 → 怎么跑 → 怎么改」的顺序拆开讲。2. 技术选型与工程骨架为什么是 C# Oracle 这套组合2.1 技术栈定位与依赖选择这套东西的核心是 .NET 平台上的 Oracle 数据访问。常见做法是用Oracle.ManagedDataAccess或Oracle.ManagedDataAccess.Core用于 .NET Core / .NET 5作为驱动它不依赖本机安装 Oracle 客户端纯托管实现部署时少一层环境折腾。实体类本身是普通 POCO不继承任何基类保持干净方便序列化和单元测试。数据访问层一般再包一层轻量的仓储Repository把OracleConnection、OracleCommand、OracleDataReader的样板代码收进去业务层只和实体打交道。选型上有几个现实考量。第一Oracle 的字段命名习惯是大写下划线USER_NAME、CREATE_TIMEC# 属性习惯是帕斯卡命名UserName、CreateTime中间必须有一层映射规则否则每个查询都要写别名。第二Oracle 没有像 SQL Server 那样顺手的自增主键序列Sequence 触发器是常见方案实体插入后要能拿回主键值这块要单独处理。第三NUMBER类型在 Oracle 里既能存整数也能存小数映射到 C# 时要看精度定义不能一刀切。依赖清单大致是这样依赖用途备注Oracle.ManagedDataAccess(.Core)数据库驱动纯托管免装客户端System.Data.Common通用 DbConnection 抽象便于换库和测试可选Dapper轻量 ORM简化映射不想手写 Reader 循环时用可选Newtonsoft.Json / System.Text.Json实体序列化接口层返回时用我一般不会一上来就上 EF Core 的 Oracle Provider存量项目里表结构不规范、字段类型混乱的情况太多轻量映射反而更可控。实体类生成这块可以用 T4 模板、也可以用一段查USER_TAB_COLUMNS的控制台程序批量生成后者更直观下面会讲。2.2 目录结构与分层一个能跑起来的工程骨架目录大致长这样OracleEntityDemo/ ├── OracleEntityDemo.sln ├── src/ │ ├── OracleEntityDemo.Model/ # 实体类 │ │ ├── Entities/ │ │ │ ├── UserInfo.cs │ │ │ └── OrderInfo.cs │ │ └── OracleEntityDemo.Model.csproj │ ├── OracleEntityDemo.Data/ # 数据访问层 │ │ ├── OracleConnectionFactory.cs │ │ ├── BaseRepository.cs │ │ ├── UserRepository.cs │ │ └── OracleEntityDemo.Data.csproj │ ├── OracleEntityDemo.Generator/ # 实体类生成器控制台 │ │ ├── Program.cs │ │ ├── ColumnMeta.cs │ │ └── TypeMapper.cs │ └── OracleEntityDemo.App/ # 入口/演示 │ └── Program.cs └── sql/ └── init_tables.sql分层意图很清楚Model只放实体不引任何数据库依赖Data引驱动负责连接和 SQLGenerator是开发期工具读 Oracle 元数据生成实体源码App是演示入口。这样拆的好处是实体层可以被接口层、单元测试直接引用不会因为引了驱动而变重。生成器单独成项目是因为它只在开发阶段跑不该混进生产程序集。2.3 连接配置与工厂封装连接字符串不要硬编码在代码里放appsettings.json或环境变量。工厂类负责按配置创建连接统一处理打开和释放// OracleConnectionFactory.cs public class OracleConnectionFactory { private readonly string _connStr; public OracleConnectionFactory(string connStr) { _connStr connStr; } // 每次调用返回一个新连接由调用方 using 释放 public OracleConnection Create() { var conn new OracleConnection(_connStr); conn.Open(); return conn; } }_connStr从配置读入形如User Idapp;Password***;Data Source127.0.0.1:1521/ORCLPDB1。这里的关键点是「每次新建、调用方释放」不要做成单例连接——Oracle 连接是有状态的多线程共用会出各种诡异问题这是血泪经验。连接池由驱动内部管理Open()实际是从池里取开销可控。参数说明Data Source的格式是host:port/service_name老式 SID 写法是host:port:SID两者别混。3. 实体类生成从 Oracle 元数据到 C# 属性的映射规则3.1 类型映射表与命名转换实体类生成的核心是一张类型映射表。Oracle 的USER_TAB_COLUMNS视图能查到表名、列名、数据类型、数据长度、精度、小数位、是否可空据此推断 C# 类型Oracle 类型条件C# 类型NUMBER无小数位且精度 ≤ 9intNUMBER无小数位且精度 ≤ 18longNUMBER有小数位decimalVARCHAR2 / NVARCHAR2 / CHAR任意stringDATE / TIMESTAMP任意DateTimeCLOB任意stringBLOB任意byte[]任意可空列IsNullable Y值类型加?命名转换规则USER_NAME→UserName按下划线切分后每段首字母大写。这里有个坑Oracle 里全大写列名查出来就是大写别指望它自动变小写转换逻辑要自己写。可空列一定要映射成可空类型否则DBNull转int直接抛异常这是新手最容易翻车的地方。3.2 生成器主流程代码生成器读元数据、拼源码字符串、写文件主流程如下// Program.cs生成器入口 static void Main(string[] args) { var factory new OracleConnectionFactory(ReadConnStr()); var tables args.Length 0 ? args : QueryAllTables(factory); foreach (var table in tables) { var columns QueryColumns(factory, table); // 查 USER_TAB_COLUMNS var code BuildEntityCode(table, columns); // 拼 C# 源码 var path Path.Combine(Entities, ${ToPascal(table)}.cs); File.WriteAllText(path, code, Encoding.UTF8); Console.WriteLine($generated: {path}); } } // 查列元数据 static ListColumnMeta QueryColumns(OracleConnectionFactory f, string table) { const string sql SELECT COLUMN_NAME, DATA_TYPE, DATA_LENGTH, DATA_PRECISION, DATA_SCALE, NULLABLE FROM USER_TAB_COLUMNS WHERE TABLE_NAME :t ORDER BY COLUMN_ID; var list new ListColumnMeta(); using var conn f.Create(); using var cmd new OracleCommand(sql, conn); cmd.Parameters.Add(:t, OracleDbType.Varchar2).Value table.ToUpper(); using var reader cmd.ExecuteReader(); while (reader.Read()) { list.Add(new ColumnMeta { Name reader.GetString(0), DataType reader.GetString(1), Length reader.GetInt32(2), Precision reader.IsDBNull(3) ? 0 : reader.GetInt32(3), Scale reader.IsDBNull(4) ? 0 : reader.GetInt32(4), Nullable reader.GetString(5) Y }); } return list; }逻辑说明USER_TAB_COLUMNS只查当前登录用户下的表如果表在别的 schema 下要换成ALL_TAB_COLUMNS并加OWNER条件。ORDER BY COLUMN_ID保证属性顺序和建表顺序一致读代码时更顺。参数用绑定变量:t别拼字符串既防注入也避免硬解析。table.ToUpper()是因为 Oracle 元数据里表名默认大写传小写查不到。3.3 拼装实体源码与特性标注拿到列元数据后拼源码时顺手加上[Column]特性方便后续映射层读取static string BuildEntityCode(string table, ListColumnMeta cols) { var sb new StringBuilder(); sb.AppendLine(using System;); sb.AppendLine(namespace OracleEntityDemo.Model.Entities;); sb.AppendLine(); sb.AppendLine($public class {ToPascal(table)}); sb.AppendLine({); foreach (var c in cols) { var csType TypeMapper.Map(c); // 按 3.1 的规则映射 var prop ToPascal(c.Name); sb.AppendLine($ [Column(\{c.Name}\)]); sb.AppendLine($ public {csType} {prop} {{ get; set; }}); sb.AppendLine(); } sb.AppendLine(}); return sb.ToString(); }TypeMapper.Map就是 3.1 那张表的代码化ColumnAttribute是自定义特性只存原始列名。这样实体类既干净又能被映射层反查列名不用在仓储里再维护一份字典。生成出来的实体大概长这样public class UserInfo { [Column(USER_ID)] public long UserId { get; set; } [Column(USER_NAME)] public string UserName { get; set; } [Column(BALANCE)] public decimal? Balance { get; set; } [Column(CREATE_TIME)] public DateTime CreateTime { get; set; } }注意Balance是可空decimal?因为源列NULLABLEY。生成器跑完实体层就齐了接下来是数据访问。4. 数据访问层封装增删改查与主键回填的完整链路4.1 通用仓储与参数绑定仓储基类把连接、命令、参数绑定收拢子类只写 SQL 和映射public abstract class BaseRepository { protected readonly OracleConnectionFactory Factory; protected BaseRepository(OracleConnectionFactory f) Factory f; // 查询列表mapper 负责 Reader - 实体 protected ListT QueryT(string sql, FuncOracleDataReader, T mapper, ActionOracleParameterCollection bind null) { var result new ListT(); using var conn Factory.Create(); using var cmd new OracleCommand(sql, conn); bind?.Invoke(cmd.Parameters); using var reader cmd.ExecuteReader(); while (reader.Read()) result.Add(mapper(reader)); return result; } // 执行非查询返回影响行数 protected int Execute(string sql, ActionOracleParameterCollection bind null) { using var conn Factory.Create(); using var cmd new OracleCommand(sql, conn); bind?.Invoke(cmd.Parameters); return cmd.ExecuteNonQuery(); } }bind委托让子类用 lambda 绑参数避免每个方法重复写cmd.Parameters.Add。参数绑定统一用OracleDbType显式指定比如Add(:name, OracleDbType.Varchar2).Value name不指定类型时驱动会猜遇到null或长字符串容易猜错。Query里mapper是FuncOracleDataReader,T子类按列序号或列名取值列名取值更稳但稍慢序号取值快但改表结构就崩我一般用列名。4.2 插入与序列主键回填Oracle 没有自增列主键靠序列。插入后要拿回主键常见做法是用RETURNING ... INTOpublic long Insert(UserInfo user) { const string sql INSERT INTO USER_INFO (USER_ID, USER_NAME, BALANCE, CREATE_TIME) VALUES (SEQ_USER_INFO.NEXTVAL, :name, :balance, :ctime) RETURNING USER_ID INTO :newId; using var conn Factory.Create(); using var cmd new OracleCommand(sql, conn); cmd.Parameters.Add(:name, OracleDbType.Varchar2).Value user.UserName; cmd.Parameters.Add(:balance, OracleDbType.Decimal).Value (object)user.Balance ?? DBNull.Value; cmd.Parameters.Add(:ctime, OracleDbType.Date).Value user.CreateTime; var idParam cmd.Parameters.Add(:newId, OracleDbType.Int64); idParam.Direction ParameterDirection.Output; cmd.ExecuteNonQuery(); return Convert.ToInt64(idParam.Value.ToString()); }逻辑说明SEQ_USER_INFO.NEXTVAL取序列下一个值作为主键RETURNING USER_ID INTO :newId把刚插入的主键回填到输出参数。注意idParam.Value取出来是OracleDecimal直接Convert.ToInt64可能报格式问题先ToString()再转更稳这是踩过的坑。可空字段传DBNull.Value而不是null否则驱动会抛参数为空异常。序列名和表名要对应上建表脚本里CREATE SEQUENCE SEQ_USER_INFO START WITH 1 INCREMENT BY 1别忘了。4.3 查询映射与分页查询映射按列名取值分页用 Oracle 12c 之后的OFFSET ... FETCHpublic ListUserInfo Page(int pageIndex, int pageSize) { const string sql SELECT USER_ID, USER_NAME, BALANCE, CREATE_TIME FROM USER_INFO ORDER BY USER_ID OFFSET :skip ROWS FETCH NEXT :take ROWS ONLY; return Query(sql, r new UserInfo { UserId r.GetInt64(r.GetOrdinal(USER_ID)), UserName r.GetString(r.GetOrdinal(USER_NAME)), Balance r.IsDBNull(r.GetOrdinal(BALANCE)) ? (decimal?)null : r.GetDecimal(r.GetOrdinal(BALANCE)), CreateTime r.GetDateTime(r.GetOrdinal(CREATE_TIME)) }, p { p.Add(:skip, OracleDbType.Int32).Value (pageIndex - 1) * pageSize; p.Add(:take, OracleDbType.Int32).Value pageSize; }); }GetOrdinal每次调用有开销字段多时可以先缓存序号数组。可空列必须用IsDBNull判断直接GetDecimal遇到NULL会抛InvalidCastException。12c 之前的老库不支持OFFSET FETCH得用ROWNUM嵌套子查询改造时先确认库版本别想当然。5. 避坑与排查C# 对接 Oracle 实体层最常见的五个坑5.1 坑一DBNull与null混用导致插入失败现象插入一条Balance为空的记录抛「参数值不能为空」或「ORA-01400 无法插入 NULL」。原因C# 的null传给OracleParameter.Value时驱动不认必须显式转DBNull.Value。解决所有可空字段统一写(object)value ?? DBNull.Value封装成扩展方法ToDbValue()在仓储里复用别每个方法手写一遍。5.2 坑二NUMBER映射成int后溢出现象某张表主键用NUMBER(19)实体生成成int数据量涨上去后查询抛溢出。原因生成器只看「无小数位」就映射int没看精度。解决按 3.1 的表精度 ≤ 9 才用int≤ 18 用long超过 18 用decimal。生成器里把DATA_PRECISION判断补上重新生成一遍实体。5.3 坑三连接字符串Data Source格式写错现象本地能连部署到另一台机器报「ORA-12514 监听程序当前无法识别连接描述符中请求的服务」。原因Data Source用了 SID 写法host:port:ORCL但目标库是服务名ORCLPDB1。解决统一用host:port/service_name格式连之前先用tnsping或让 DBA 确认服务名。连接串放配置中心别写死在代码里换环境只改配置。5.4 坑四生成器查不到表现象生成器跑完输出「generated: 0 个文件」或查列返回空。原因USER_TAB_COLUMNS只查当前登录用户 schema表建在别的 schema 下就查不到或者表名传了小写。解决改用ALL_TAB_COLUMNS加OWNER :owner条件表名统一ToUpper()。生成前先SELECT COUNT(*) FROM ALL_TAB_COLUMNS WHERE OWNER...验证一下别盲跑。5.5 坑五RETURNING INTO输出参数取值异常现象插入成功但回填主键抛「指定的转换无效」。原因OracleParameter.Value返回的是OracleDecimal包装类型直接强转long失败。解决先idParam.Value.ToString()再Convert.ToInt64或者用((OracleDecimal)idParam.Value).Value取原始值。这个坑很隐蔽因为插入本身是成功的只有回填那一步炸。6. 二次开发改造把生成器变成可持续维护的实体层工具这套东西真正有价值的地方不在「生成一次实体」而在「表结构变了能低成本重新生成并安全合并」。我一般会做三件事。第一给生成器加一个--diff模式生成到临时目录和现有实体目录逐文件比对只输出有变化的文件列表人工确认后再覆盖避免把手工加的注释、计算属性冲掉。第二实体类上加partial关键字生成器只写主文件手工扩展写在另一个UserInfo.Extend.cs里重新生成不影响扩展。第三把类型映射规则抽成配置JSON 或代码里的字典遇到特殊列比如NUMBER(1)想映射成bool可以按表名列名覆盖不用改生成器源码。验证方法也简单改完生成器后拿一张字段类型最全的表跑一遍把生成的实体和数据库实际类型逐列对一遍再写个插入-查询-断言的集成测试覆盖可空列、大字段、序列主键三种情况。集成测试连真实库别用内存库糊弄Oracle 的类型行为差异太大。// 集成测试片段验证可空列与主键回填 [Fact] public void Insert_And_Read_Back() { var repo new UserRepository(TestFactory); var id repo.Insert(new UserInfo { UserName tester, Balance null, // 可空列 CreateTime DateTime.Now }); Assert.True(id 0); var loaded repo.GetById(id); Assert.Equal(tester, loaded.UserName); Assert.Null(loaded.Balance); // 回读仍为 null }从那以后我每次改表结构都强制走一遍「生成到临时目录 → diff → 确认 → 覆盖 → 跑集成测试」这条链路再没出现过实体和表对不上的情况。这套流程不复杂但能省下大量排查类型转换异常的时间。希望帮到你。本文还有配套的精品资源点击获取