
简介面向C#/.NET开发者与Oracle数据库应用维护人员的实体类生成工具包围绕自动生成与表结构对应的C#实体类展开缓解手写映射代码繁琐、易错的问题。压缩包共62个文件以cs源码、dll依赖库、xaml界面文件及xml配置为主另有csproj、sln等工程文件与少量资源图标整体约2.99MB可直接在Visual Studio中打开研究。已有214人学习下载。内容涵盖Oracle连接字符串配置、表与视图的实体类生成、主键与表名注解、字段类型映射以及DbContext上下文和LINQ增删改查示例同时涉及Code First迁移、Repository与Unit of Work等扩展思路便于读者理解ORM映射链路并据此改造为适合自身项目的代码生成与数据访问方案。1. 从 Oracle 表结构倒推 C# 实体类一份能直接跑的生成器拆解接一个跑了七八年的 Oracle 库第一件事往往不是写查询而是给几十上百张表补齐 C# 实体类。手敲三五张还行到第五十张就开始复制粘贴加改名然后某个可空列漏了个问号线上抛出一个空引用。这份资源就是冲这个场景来的一套基于数据字典的 C# 实体类生成器读ALL_TAB_COLUMNS、ALL_CONSTRAINTS这些系统视图按配置把 Oracle 表结构翻译成 C# POCO带类型映射表、可空判断、命名风格转换和一份批量生成入口。适合正在用 C# 对接 Oracle、又不想被重量级 ORM 绑死的人尤其适合已经用 Dapper 手工写 SQL、只缺实体层的团队。下面按“怎么选、元数据怎么读、类型怎么映射、哪里会翻车、生成完怎么验”的顺序拆照着敲能跑出第一批实体。2. 手写、ORM 反推与元数据生成三条路线怎么选2.1 手写 POCO 的死线在哪里前二十张表手写其实没什么问题反而更快——你知道每个字段的业务含义能顺手写上注释和校验。问题出在第二十一张之后以及每次表结构变更的时候。手写实体的真实成本不在第一次写而在没人记得同步。一段典型的手写实体长这样// 手写实体的典型形态属性名是照着 C# 习惯改的但和库里的大写列名对不上 public class OrderItem { public decimal OrderId { get; set; } public string ProductCode { get; set; } public DateTime CreatedAt { get; set; } }代码本身没问题问题在映射。Oracle 里这张表的列名是ORDER_ID、PRODUCT_CODE、CREATED_AT未加引号的标识符在入库时会被折叠成大写。Dapper 默认的列名匹配只做大小写不敏感比较不会帮你把下划线去掉于是ORDER_ID匹配不上OrderId属性静静躺在默认值上你还以为查到了数据。要绕过去要么每个查询都写SELECT ORDER_ID AS OrderId要么在生成阶段就把属性名和列名对齐。前者是体力活后者才是一次性投入。手写还有两个隐性成本。一是可空性Oracle 的NULLABLE是Y/NC# 里对应的是int?和int、string?和string手写时全靠脑子记漏一个就等着线上报错。二是类型精度NUMBER(19,4)到底是decimal还是double不同的人写出来不一样同一个项目里能凑出三种风格。2.2 ORM 反向生成能做到哪一步第二条路线是把实体层的活交给 ORM 的数据库优先脚手架。以常见的 EF Core 为例一条命令就能把整库的表反推成DbContext加实体# 从现有 Oracle 库反推实体与 DbContext输出到 Models 目录 dotnet ef dbcontext scaffold \ User Idapp;Password***;Data Source10.0.0.12:1521/ORCLPDB \ Oracle.EntityFrameworkCore \ --output-dir Models \ --context-dir Data \ --no-onconfiguring \ --use-database-names参数需要逐个交代清楚。第二个位置参数是驱动包名必须和项目里引用的版本一致--output-dir决定实体落盘位置--no-onconfiguring表示不把连接字符串硬编码进OnConfiguring避免凭据进仓库--use-database-names让生成的属性名保持库里的原始大小写不加这个参数脚手架会按自己的规则改写反而更难对照。这条路线的好处是快几分钟就能拿到一整套能跑的上下文。但它有几个绕不开的边界。第一它会生成大量导航属性和反向集合存量库里的外键往往建得不规范生成出来的关系图可能互相缠绕编译能过但用起来很别扭。第二视图、同义词、复合主键、分区表这些对象脚手架的覆盖程度参差不齐遇到报错时排查成本不比手写低。第三如果你本来就打算用 Dapper 写 SQL那引入整个 ORM 只为了拿实体等于为了喝牛奶养了一头牛。2.3 元数据驱动的生成器骨架第三条路线是自己读数据字典生成。它的定位很明确只解决“表结构 → C# 类”这一件事不引入运行时依赖生成出来的就是普通 POCO后面接 Dapper、接手写 ADO.NET、接任何 ORM 都可以。先把工程建起来注意生成器要独立于业务项目避免把 Oracle 驱动依赖带进主程序# 生成器单独一个控制台工程只在开发机上跑 dotnet new console -n EntityGen cd EntityGen dotnet add package Oracle.ManagedDataAccess.Core第一条命令创建控制台模板第二条进入目录第三条引入托管版 Oracle 驱动。这里建议用托管版而不是依赖本机安装的客户端版本理由是生成器通常在开发机或 CI 上跑托管版不需要额外装客户端部署面小版本也好锁。参数上唯一要注意的是包版本要和业务项目保持一致否则生成的代码在业务侧编译时可能遇到类型不匹配。三条路线的取舍可以放在一张表里看路线适合规模初次成本结构变更后的维护成本运行时依赖手写 POCO20 张表以内低高靠人记无ORM 反向生成50 张表以内、关系简单低中重新生成即覆盖绑定 ORM元数据驱动生成不限尤其存量库中低重跑脚本无产物是普通类选第三条的理由不是它更先进而是它把“易错的部分”交给机器列名到属性名的转换、可空性判断、类型精度映射这三件事人工做一定会出错而且错得很安静。生成器写一次之后每次表结构变更重跑一遍差异是确定的。3. 读透 Oracle 数据字典ALL_TAB_COLUMNS 里的五个关键字段3.1 列级元数据类型、长度、精度、可空生成实体的全部输入基本都能从ALL_TAB_COLUMNS拿到。这条查询是整个生成器的心脏-- 读列级元数据类型、字节长度、字符长度、精度、标度、可空、默认值 SELECT c.OWNER, c.TABLE_NAME, c.COLUMN_ID, c.COLUMN_NAME, c.DATA_TYPE, c.DATA_LENGTH, c.CHAR_LENGTH, c.DATA_PRECISION, c.DATA_SCALE, c.NULLABLE, c.DATA_DEFAULT FROM ALL_TAB_COLUMNS c WHERE c.OWNER :owner AND c.TABLE_NAME :tableName ORDER BY c.COLUMN_ID;参数只有两个owner是 schema 名tableName是表名两个都用绑定变量传不要拼字符串——表名里出现特殊字符时拼串会直接语法错误而且拼串写法在评审时过不了。COLUMN_ID决定列在表里的物理顺序生成实体时按它排序产物的字段顺序和表结构一致对照起来省事。各字段的含义和用途需要说清楚DATA_TYPE是类型名VARCHAR2、NUMBER、DATE、TIMESTAMP(6)、CLOB这些都是它的取值映射规则完全依赖它DATA_LENGTH是字节长度CHAR_LENGTH是字符长度。对VARCHAR2(50 CHAR)这种按字符声明的列DATA_LENGTH可能是 200按 UTF-8 估算CHAR_LENGTH才是 50。判断字符串长度上限时用后者DATA_PRECISION和DATA_SCALE只对数值类型有意义。精度是总位数标度是小数位数NUMBER(19,4)对应精度 19、标度 4NULLABLE只有Y和N两个值直接决定 C# 那边要不要加问号DATA_DEFAULT是列的默认值表达式类型是LONG读取时能拿到原始文本但别在 SQL 里对它做TO_CHAR或字符串拼接会报错。需要处理默认值就在 C# 侧读出来再解析。提示DATA_DEFAULT在部分老版本里返回的是LONG某些客户端工具展示时会截断。生成器里如果要用它取出来之后立刻转成string别留在IDataReader上反复访问。权限上还有个常见问题ALL_TAB_COLUMNS只显示当前用户有权限访问的对象。如果生成器连的账号不是对象属主可能读不全。换用DBA_TAB_COLUMNS需要额外授权一般做法是给生成器单独开一个只读账号把目标 schema 的查询权限授全别图省事直接用业务账号跑。3.2 主键、自增与序列ALL_CONSTRAINTS 与 ALL_TAB_IDENTITY_COLS只有列信息还不够实体类要不要标主键、主键是单列还是复合、有没有自增这些得从约束视图里捞。主键查询这样写-- 主键列与列在复合主键中的顺序POSITION 从 1 开始 SELECT cc.COLUMN_NAME, cc.POSITION FROM ALL_CONSTRAINTS ac JOIN ALL_CONS_COLUMNS cc ON cc.OWNER ac.OWNER AND cc.CONSTRAINT_NAME ac.CONSTRAINT_NAME WHERE ac.OWNER :owner AND ac.TABLE_NAME :tableName AND ac.CONSTRAINT_TYPE P ORDER BY cc.POSITION;CONSTRAINT_TYPE P过滤出主键约束POSITION给出复合主键里的列序。这里有个细节ALL_CONS_COLUMNS的关联条件是OWNER加CONSTRAINT_NAME不能只关联约束名因为不同 schema 下可能有同名约束。生成实体时把主键列收集成一个HashSetstring渲染时在属性上打标记后续写仓储代码可以直接用。自增列要看版本。较新的库支持 identity 列从ALL_TAB_IDENTITY_COLS读-- 12c 之后的 identity 列老库这一句会报对象不存在需要 try/catch 兜住 SELECT tc.COLUMN_NAME, tc.GENERATION_TYPE FROM ALL_TAB_IDENTITY_COLS tc WHERE tc.OWNER :owner AND tc.TABLE_NAME :tableName;GENERATION_TYPE是ALWAYS或BY DEFAULT前者表示不允许显式插入后者允许。生成器读到这一列就在实体上加一个注释说明是自增列插入方法里跳过它。老库没有 identity 概念自增靠序列加触发器实现那就退回到ALL_SEQUENCES查序列名或者干脆不管——实体类本身不需要知道序列那是仓储层的事。还有一类对象容易被忘掉虚拟列。虚拟列在ALL_TAB_COLUMNS里有记录但它的值由表达式算出来不能写。判断依据是ALL_TAB_COLS.VIRTUAL_COLUMN YES注意是ALL_TAB_COLS不是ALL_TAB_COLUMNS。生成器如果不处理用户拿到实体往里赋值写库时直接报错。3.3 把查询结果落成中间模型从数据字典读出来的东西不要直接往字符串模板里塞先落成一个中间模型。这一步多写几十行后面改映射规则时能省掉大量返工// 中间模型只承载生成实体所需的信息和业务实体完全解耦 public sealed record OracleColumn( string Name, // 原始列名全大写或带引号的原样 string DataType, // VARCHAR2 / NUMBER / TIMESTAMP(6) ... int? Precision, // NUMBER 的总位数其它类型为 null int? Scale, // NUMBER 的小数位其它类型为 null int CharLength, // 字符长度字符串类型判断上限用 bool IsNullable, // NULLABLE Y int Ordinal, // COLUMN_ID决定属性顺序 bool IsPrimaryKey, // 从约束视图合并进来 string? DefaultValue // DATA_DEFAULT 原文可能为 null ); // 一张表的完整描述表和列一起传进渲染器 public sealed record OracleTable( string Owner, string Name, IReadOnlyListOracleColumn Columns);这里用record而不是class是因为中间模型生成之后只读不改值语义更符合用途。Ordinal保存COLUMN_IDIsPrimaryKey是在读取阶段就把主键查询的结果合并进去而不是留到渲染阶段再查一次——少一次往返也避免了渲染器依赖数据库连接。DefaultValue保留原文是有意的。Oracle 的默认值表达式千奇百怪SYSDATE、N、0都可能出现硬解析容易出错。生成时先原样写进注释需要转成 C# 默认值的场景再单独处理。这一步做完生成器的输入输出就固定了输入是OracleTable输出是一个字符串。中间任何环节出问题都能单独替换。4. 类型映射与实体生成从 NUMBER(10,0) 到 int? 的完整对照4.1 一张表看完 Oracle 到 C# 的映射规则类型映射是整个生成器最容易写错的地方错法还很隐蔽——编译能过运行时才炸。先把对照表定下来Oracle 类型C# 类型判定条件与说明NUMBER(p,0)p ≤ 10int/int?标度为 0 且精度不超过 10 位NUMBER(p,0)10 p ≤ 18long/long?同上位数放宽NUMBER(p,s)s 0decimal/decimal?金额、比率一律用 decimalNUMBER无精度无标度decimal/decimal?裸 NUMBER 可存 38 位精度绝不能当 intBINARY_FLOATfloat/float?BINARY_DOUBLE、FLOATdouble/double?DATEDateTime/DateTime?自带时分秒不要当纯日期用TIMESTAMP(n)DateTime/DateTime?纳秒精度在 C# 侧会损失够用即可TIMESTAMP WITH TIME ZONEDateTimeOffset/DateTimeOffset?带时区偏移用 DateTime 会丢信息VARCHAR2、NVARCHAR2、CHAR、NCHARstring引用类型可空性用?表达CLOB、NCLOBstring大文本读取时注意流式处理BLOB、RAWbyte[]二进制注意大对象的内存占用ROWIDstring映射函数写成纯函数输入OracleColumn输出 C# 类型字符串// 类型映射集中在一处改规则只动这个类 public static string ToCSharp(OracleColumn col) { // 去掉 TIMESTAMP(6) 这类带参数的括号只留基名 var baseType col.DataType.Split(()[0].Trim().ToUpperInvariant(); string core baseType switch { NUMBER when col.Scale is 0 decimal, NUMBER when col.Precision is null || col.Scale is null decimal, // 裸 NUMBER NUMBER when col.Precision 10 int, NUMBER when col.Precision 18 long, NUMBER decimal, BINARY_FLOAT float, BINARY_DOUBLE or FLOAT double, DATE DateTime, TIMESTAMP DateTime, TIMESTAMP WITH TIME ZONE DateTimeOffset, TIMESTAMP WITH LOCAL TIME ZONE DateTime, CLOB or NCLOB string, BLOB or RAW byte[], CHAR or NCHAR or VARCHAR2 or NVARCHAR2 or ROWID string, _ object // 兜底遇到未覆盖类型时人工介入 }; // 值类型才加可空标记引用类型在启用可空上下文时同样加 return col.IsNullable ? core ? : core; }这段逻辑有三处值得说明。第一NUMBER的分支顺序不能乱when子句从上往下匹配必须先把“标度大于 0”和“裸 NUMBER”这两个特例处理掉再判精度。裸 NUMBER 可以存小数映射成int会直接截断。第二TIMESTAMP的括号参数被Split(()去掉了所以TIMESTAMP(6)和TIMESTAMP(9)走同一个分支精度差异在 C# 侧本来就是DateTime的 100 纳秒刻度没必要区分。第三兜底返回object而不是抛异常是为了让批量生成能跑完最后汇总一份“未映射类型清单”让人工确认而不是第一张表就中断。注意NUMBER(10)和NUMBER(10,0)在数据字典里可能表现不同前者DATA_SCALE为 0后者也是 0但某些情况下DATA_SCALE会是 null。判断时用col.Scale is null || col.Scale 0更稳妥。4.2 用模板批量生成实体类渲染环节有两种做法T4 模板和字符串拼接。T4 在 Visual Studio 里体验好但它依赖设计时环境放在 CI 上跑比较别扭字符串拼接土一点胜在零依赖、行为可预测。这套生成器用的是后者// 渲染单张表的实体输出可直接编译的 C# 源码 public static string Render(string ns, OracleTable table, IReadOnlySetstring pkColumns) { var sb new StringBuilder(); sb.AppendLine(// auto-generated/); sb.AppendLine(// 由 EntityGen 根据数据字典生成不要手工修改改表结构后重新生成); sb.AppendLine(#nullable enable); sb.AppendLine(using System;); sb.AppendLine(); sb.AppendLine($namespace {ns};); sb.AppendLine(); sb.AppendLine($public sealed class {Naming.Pascal(table.Name)}); sb.AppendLine({); foreach (var col in table.Columns.OrderBy(c c.Ordinal)) { var csType TypeMap.ToCSharp(col); var propName Naming.Pascal(col.Name); // 主键列打注释仓储层可以直接按注释约定扫 if (pkColumns.Contains(col.Name)) { sb.AppendLine($ // PRIMARY KEY: {col.Name}); } // 非空的引用类型给默认值避免生成可空警告 var initializer csType string ? string.Empty; : string.Empty; sb.AppendLine($ public {csType} {propName} {{ get; set; }}{initializer}); } sb.AppendLine(}); return sb.ToString(); }几个关键点。// auto-generated/这行标记会让部分静态分析工具跳过这个文件避免生成代码刷出一屏警告。#nullable enable显式打开可空上下文配合类型映射里加的问号编译器能帮你盯住非空约束。Naming.Pascal负责把ORDER_ID转成OrderId这个转换函数要单独写并配单元测试USER_ID、ID、A_B_C这些边界情况都得覆盖。字符串类型加 string.Empty初始化是因为 Oracle 里NOT NULL的VARCHAR2列在 C# 侧生成为非空string如果不初始化编译器会给警告而且从数据库读到NULL历史脏数据时会变成null赋给非空属性。主键信息以注释形式写进产物而不是用特性标注。这是个取舍特性需要引入额外依赖或者自定义特性类注释零成本仓储层用正则扫一下就能拿到主键列。如果项目里已经有统一的实体基类或标注约定改成特性也就是几行的事。4.3 生成入口与命令行参数把读取、映射、渲染串起来的主流程重点是错误处理策略——批量生成时不能因为一张表出错就整体中断// 主流程先取表清单再逐表生成单表失败记录后继续 var tables reader.ListTables(owner, tableFilter); // tableFilter 支持通配如 T_% Console.WriteLine($匹配到 {tables.Count} 张表); var failed new List(string Table, string Reason)(); foreach (var table in tables) { try { var columns reader.ReadColumns(owner, table); // 内部合并主键、可空、精度 var code EntityWriter.Render(ns, columns.Table, columns.PrimaryKeys); var path Path.Combine(outDir, Naming.Pascal(table) .cs); File.WriteAllText(path, code); Console.WriteLine($[OK] {table} - {path}); } catch (Exception ex) { // 单表失败不影响其余表最后统一汇总 failed.Add((table, ex.Message)); Console.Error.WriteLine($[FAIL] {table}: {ex.Message}); } } Console.WriteLine($完成失败 {failed.Count} 张); return failed.Count 0 ? 0 : 1;tableFilter参数支持%通配对应 SQL 里的LIKE第一次跑建议只匹配两三张表试水别一上来就全库。outDir是输出目录生成前先清空或者按表名覆盖避免残留上一次的旧文件——这一点很重要表改名之后旧实体还在目录里编译时会出现两个同名类。返回码设计成“有失败就非零”方便在 CI 里做门禁。命令行参数建议直接用手写解析别为了三个参数引入一个框架# 生成指定 schema 下以 T_ 开头的表输出到当前项目的 Models 目录 dotnet run --project EntityGen -- \ --owner APP \ --filter T_% \ --out ./Models \ --namespace MyApp.Models--owner是 schema 名大小写敏感全大写是常规写法--filter是表名通配--out决定产物落盘位置--namespace写进生成文件的namespace声明必须和业务项目的命名空间一致否则引用时要额外 using。连接字符串不要放命令行参数从环境变量读避免进 shell 历史。5. 避坑Oracle 实体类生成最容易翻车的五处5.1 标识符层面全大写列名与带引号的对象坑一属性永远是默认值数据库里明明有数据。现象是查询返回了行但实体上某个属性一直是0或null断点看IDataReader里字段名是ORDER_ID实体属性叫OrderId。原因是 Oracle 对未加引号的标识符统一折叠成大写而 Dapper 的默认映射只做大小写不敏感比较不会做下划线到驼峰的转换两个名字对不上。解决办法有两个方向一是在 SQL 里显式起别名SELECT ORDER_ID AS OrderId注意别名要加双引号才能保留大小写二是在生成器里统一用 Pascal 命名再配一个自定义的ITypeMap把列名和属性名对齐。长期看第二种更省事但需要给 Dapper 配一次映射规则配置放在数据访问层的初始化里别散落在各个仓储。坑二表在ALL_TAB_COLUMNS里查不到或者查询报表不存在。现象是生成器提示“匹配到 0 张表”但用工具连上去明明能看到。原因是建表时表名加了双引号比如CREATE TABLE OrderDetail这种对象的真实名字就是大小写混合的而查询里写WHERE TABLE_NAME ORDERDETAIL匹配不上。更麻烦的是拼 SQL 时如果不加引号Oracle 会把名字折叠成大写再去查直接报ORA-00942。解决办法是生成器内部保留两份名字一份是数据字典里的原始值用于拼 SQL 时加引号一份是折叠成大写后的值用于匹配和过滤。渲染 C# 类名时用原始值转 Pascal不要用折叠后的。5.2 类型与空值层面NUMBER 精度、DATE 时间和 DBNull坑三金额字段算出来对不上小数部分被吃掉。现象是NUMBER列存了1234.56读出来变成1234。原因是类型映射时把裸NUMBERDATA_PRECISION和DATA_SCALE都为空当成了整数映射成int。裸 NUMBER 能存 38 位有效数字加小数位是 Oracle 里最灵活也最危险的数值类型。解决办法是把“精度为空”作为独立分支直接映射decimal只有在DATA_SCALE 0且DATA_PRECISION有值时才考虑整型。改完映射规则后把库里所有NUMBER列的DATA_PRECISION、DATA_SCALE拉出来过一遍人工确认没有漏网的特殊列。坑四时间部分莫名丢失插进去永远是零点。现象是 Oracle 里DATE类型的列存了时分秒C# 侧写回去之后时间变成00:00:00。原因是 Oracle 的DATE其实包含年月日时分秒但很多人在 C# 里用DateTime.Date或者只取日期部分赋值写库时自然就截断了。另一个来源是参数类型给OracleParameter设OracleDbType.Date时驱动会按日期处理需要完整时间就用OracleDbType.TimeStamp或者直接传DateTime让驱动推断。解决办法是实体统一用DateTime仓储层插入时参数类型显式指定TimeStamp需要毫秒以上精度就把列改成TIMESTAMP(6)别在DATE上纠结。坑五非空字符串属性遇到数据库里的 NULL 直接抛异常。现象是接口报NullReferenceException堆栈指向属性访问。原因是生成器把VARCHAR2一律生成成了非空的string但历史数据里这个列存在NULL约束是后来加的或者用NOT NULL但绕过约束写入过。解决办法是生成时严格按NULLABLE判断Y生成string?N生成string并给string.Empty初始化同时在仓储层读取时统一做一次DBNull判断DBNull.Value转成null或空串别让DBNull流到业务层。这一步有个后悔药在读数据的公共方法里加一层转换比在几百个实体上打补丁划算得多。5.3 环境层面驱动版本与连接字符串坑六本地能跑换台机器就报“找不到程序集”。现象是生成器在开发机上正常在 CI 或者同事机器上抛FileNotFoundException提示某个驱动程序集加载失败。原因是项目里间接引用了多个版本的 Oracle 驱动运行时加载到了错误的那个或者用了依赖本机客户端的非托管版本而那台机器没装客户端。解决办法是生成器工程只引托管驱动并且在csproj里显式锁定版本不要用浮动版本号业务工程和生成器工程各自维护依赖不要共享bin目录。连接字符串这边也有个高频问题Data Source写成host:port:SID和host:port/service_name是两种格式写错了分别报ORA-12505和ORA-12514遇到连接错误先把这两段拆开单独验证。6. 生成之后先验哪三张表一份可复跑的校验清单生成器跑通不代表产物能用。我一般不会一次性生成全库而是先挑三张有代表性的表试跑确认三件事都能对上再放开。第一张是带复合主键的表用来验证主键查询和POSITION排序是否正确。检查点是生成文件顶部的PRIMARY KEY注释数量对得上列顺序和ALL_CONS_COLUMNS.POSITION一致。第二张是含 LOB 字段的表验证CLOB映射成string、BLOB映射成byte[]而且读取时不会因为大字段把内存撑爆——取一条数据看CLOB内容是否完整。第三张是含小数和可空列的表验证NUMBER(19,4)生成了decimal?而不是intNULLABLEY的字符串列带了问号。三张表过完之后用一个小脚本做批量对拍把生成实体的属性名和数据库列名做一次双向比对// 对拍属性名转回大写下划线和列名集合比较缺一或多一都报出来 var props typeof(Order).GetProperties() .Select(p Naming.ToSnakeUpper(p.Name)) // OrderId - ORDER_ID .ToHashSet(StringComparer.OrdinalIgnoreCase); var cols reader.ReadColumns(APP, ORDERS).Table.Columns .Select(c c.Name) .ToHashSet(StringComparer.OrdinalIgnoreCase); var missing cols.Except(props).ToList(); // 库里有、实体里没有 var extra props.Except(cols).ToList(); // 实体里有、库里没有 Console.WriteLine($缺列 {missing.Count}多列 {extra.Count});ToSnakeUpper是Pascal的逆运算专门用来对拍实现上就是把每个大写字母前面补下划线再整体转大写。Except用OrdinalIgnoreCase比较器避免大小写差异误报。缺列说明生成器没覆盖到多半是虚拟列或权限问题多列说明实体没重新生成文件是旧的。验收标准很简单缺列和多列都为零三张样表的增删改查各跑一遍不报错生成目录里没有重复类名。做到这一步剩下的表可以批量跑跑完再抽查几张。从那以后我每接一个 Oracle 库都先把生成器在三张样表上跑通再放开全库宁可前面多花半小时也不想在几百个生成文件里找一个漏掉的可空标记。希望帮到你。本文还有配套的精品资源点击获取