EF Core + PostgreSQL 蛇形命名:一行配置与自定义约定实战
发布时间:2026/9/26 11:21:11来源:尧图网络
最近接了一个新项目技术栈是 ASP.NET Core EF Core 8 CodeFirst PostgreSQL。团队里 DBA 和运维同事有一个硬性要求数据库表名、字段名必须用蛇形命名法snake_case也就是created_at、is_deleted这种风格。但 C# 这边的类名、属性名按照团队规范又是标准的驼峰命名PascalCase比如CreatedAt、IsDeleted。这两套命名规范本身各有道理问题是 EF Core CodeFirst 默认生成表结构时表名直接用 DbSet 属性名字段名直接用实体属性名出来的全是CreatedAt、OrderItems这种带引号的驼峰标识符。PostgreSQL 对带引号的标识符是严格区分大小写的手写 SQL 的时候非常痛苦。这篇文章就把我在实际项目里解决这个问题的完整过程写下来。核心结论先放在前面用 Npgsql 提供的UseSnakeCaseNamingConvention()一行代码就能搞定大部分需求如果遇到特殊场景再基于IModelFinalizingConvention自定义约定做精细化控制。我会把这两种方案的原理、代码、迁移结果和容易翻车的细节都过一遍给正在做 EF Core PostgreSQL 选型或者准备统一命名规范的同学一个可以直接参考的落地方案。1. 为什么 PostgreSQL 项目里特别需要蛇形命名1.1 PostgreSQL 的标识符大小写语义和 MySQL 差异很大很多从 MySQL 转过来的同学会在 PostgreSQL 上栽跟头因为两者对标识符大小写的处理逻辑完全不同。MySQL 在 Windows 上默认对表名大小写不敏感在 Linux 上对表名敏感但列名基本不区分大小写PostgreSQL 则把标识符分成两类不加双引号的标识符会被折叠成小写加了双引号的标识符则严格区分大小写。这就造成了一个很尴尬的局面。EF Core 默认生成的 SQL 是这样的SELECT o.Id, o.CreatedAt, o.Title FROM Orders AS o WHERE o.Id 1;这条 SQL 在 PostgreSQL 里其实能正常执行因为 EF Core 生成的 SQL 里给每个表名和列名都加了双引号PostgreSQL 会按原样解析。但问题在于你在 pgAdmin、DataGrip、Navicat 这类数据库管理工具里看到的表名和列名也是带着双引号的Orders、CreatedAt。如果你自己写 SQL 排查数据SELECT Id, CreatedAt FROM Orders;这条 SQL 大概率会直接报relation orders does not exist。因为不带引号的Orders被折叠成了小写orders而真实表名是带引号的Orders两者不匹配。必须老老实实写成SELECT Id, CreatedAt FROM Orders;一次两次还能忍时间长了谁受得了。尤其是团队里有 DBA、数据分析师、运维同学他们不是 .NET 开发没有 EF Core 帮你拼 SQL全靠手工写脚本。一套全驼峰的表结构在 PostgreSQL 里就是一场灾难。1.2 从 C# 命名到数据库命名的天然冲突C# 语言本身的命名规范就是 PascalCase类名用BlogPost属性名用CreatedAt这是从语言层面就定死的习惯。EF Core 作为 CodeFirst 框架它的默认行为就是类名即表名、属性名即列名这在 SQL Server 上问题不大因为 SQL Server 对大小写不敏感CreatedAt和createdat在使用上没有区别。但 PostgreSQL 不是这样。前面说了PostgreSQL 的存储语义决定了它更适合created_at、blog_post这种全小写加下划线的风格。数据库生态里的各种工具、运维脚本、监控系统默认也是按 snake_case 来解析元数据的。EF Core 虽然能把带引号的驼峰标识符跑通但整个 PostgreSQL 生态并不买账。另外还有一个很现实的问题数据同步工具和 CDC 组件。很多增量同步软件在解析 PostgreSQL 的 WAL 日志或者系统目录时对带引号的驼峰列名处理得并不好经常出现字段映射错乱。而 snake_case 是全小写所有工具都能正确识别省掉一堆兼容性问题。1.3 这是团队协作问题不只是个人偏好我在项目里推动蛇形命名的另一个原因是代码审查和 SQL 审查的效率。DBA 同事审核表结构时看到created_at、updated_at一眼就知道含义看到CreatedAt、UpdatedAt还得确认是不是驼峰风格、需不需要加引号来回沟通的成本非常高。统一命名规范之后业务开发写 SQL 不用再纠结大小写问题DBA 写巡检脚本也不用特意绕开带引号的标识符。这套规范一旦在团队的数据库设计文档里定下来EF Core 这边就必须跟上。所以问题的本质是C# 侧保持 PascalCase 不动数据库侧映射到 snake_case两边各管各的。EF Core CodeFirst 要做的就是在生成模型的时候把表名和列名做一次约定转换而不是让开发人员在每个实体类上手动标注[Table]和[Column]。2. EF Core CodeFirst 的命名生成机制到底发生在哪一步2.1 从实体类型到关系模型表名和列名是怎么被决定的要理解命名策略得先知道 EF Core 的模型构建流程。EF Core 启动时会把所有实体类通过DbContext注册到ModelBuilder然后经过一系列约定Convention的处理最终生成一个IModel。这个IModel才是 EF Core 生成 SQL、执行迁移的真正依据。IModel内部的核心对象是IEntityType实体类型和IProperty属性。IEntityType会对应一张表默认表名取自DbSet属性名如果用的是SetT()方式注册则取实体类型名IProperty对应一个列默认列名就是 C# 属性名。整个过程中表名和列名并不是写死的字符串而是存在模型里的注解Annotation中。EF Core 内置了非常多约定比如DbSetNameConvention负责设定表名PropertyDiscoveryConvention负责发现属性KeyDiscoveryConvention负责寻找主键。命名策略就是这些约定在模型构建完成前做最后的统一改写。你可以在模型的 finalizing 阶段拿到所有已经计算好的表名、列名、索引名然后统一替换成 snake_case。这个阶段就是IModelFinalizingConvention约定接口的切入点。我画一条简化的流程线实体类定义 - DbContext 注册 - 约定发现属性/关系 - 生成默认表名/列名 - 模型 finalizing - 命名策略转换 - 迁移/查询关键点在于模型 finalizing这一步。EF Core 把所有需要转换的标识符都放在这一环处理这就是为什么改动命名策略不需要动任何实体类完全可以在模型层面统一拦截。2.2 Npgsql 内置命名策略的原理和用法Npgsql.EntityFrameworkCore.PostgreSQL很早就注意到 PostgreSQL 社区对 snake_case 的需求所以从 6.0 版本开始提供了UseSnakeCaseNamingConvention()扩展方法。这个方法来自Npgsql.EntityFrameworkCore.PostgreSQL命名空间使用起来非常简洁optionsBuilder.UseNpgsql( Hostlocalhost;Port5432;Databaseblogdb;Usernamepostgres;Passwordyour_password, o o.UseSnakeCaseNamingConvention());它做的事情本质上就是注册了一个模型约定在模型 finalizing 阶段把默认约定生成的所有标识符——表名、列名、索引名、外键约束名、主键约束名——统一做一次 snake_case 转换。转换只改数据库侧标识符C# 类名和属性名完全不受影响。有同学可能会问为什么不让实体类本身改成 snake_case这当然也是一种办法比如把属性写成created_at。但 C# 里这么做会破坏命名规范还会影响 JSON 序列化、DTO 映射、代码可读性。用命名策略的方式实体类保持优雅的 PascalCase数据库层自动转换成合作方要求的格式这才是官方推荐的做法。2.3 自定义约定是更底层的控制手段如果你用的是 SQL Server或者你需要同时兼容多种数据库又或者 Npgsql 内置策略在某些细节上不满足你的需求那就需要自己实现命名约定。EF Core 提供了IModelFinalizingConvention接口允许你在模型 finalizing 阶段直接改写IEntityType的表名和IProperty的列名。这个方案的灵活度比 Npgsql 内置策略更高但也意味着你得自己处理更多边界情况。第三部分我会给出完整实现这里先记住一个结论内置策略适合 90% 的标准场景自定义约定适合剩下 10% 的特殊场景。3. 实操先用 Npgsql 一行切换再用迁移验证3.1 准备 Demo 项目和实体模型为了把整个流程讲清楚我准备了一个最小可运行的 Demo。先建一个控制台项目或者 ASP.NET Core WebAPI 项目然后安装对应版本的 NuGet 包。dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL --version 8.0.0实体类保持正常的 C# PascalCase 风格public class Blog { public int Id { get; set; } public string Title { get; set; } public string Summary { get; set; } public DateTime CreatedAt { get; set; } public int AuthorId { get; set; } public Author Author { get; set; } } public class Author { public int Id { get; set; } public string FullName { get; set; } public string Email { get; set; } public ListBlog Blogs { get; set; } }这段代码里没有任何[Table]、[Column]特性所有命名都交给约定处理。3.2 在 DbContext 中开启 SnakeCase 约定DbContext的OnConfiguring方法里加上一行public class AppDbContext : DbContext { public DbSetBlog Blogs { get; set; } public DbSetAuthor Authors { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseNpgsql( Hostlocalhost;Port5432;Databasesnakecase_demo;Usernamepostgres;Passwordyour_password, o o.UseSnakeCaseNamingConvention()); } }如果是 ASP.NET Core 项目一般在Program.cs里配置builder.Services.AddDbContextAppDbContext(options options.UseNpgsql(connectionString, o o.UseSnakeCaseNamingConvention()));两种写法效果一样都是告诉 Npgsql 在生成数据库标识符时使用 snake_case 策略。这里有个容易忽略的细节UseSnakeCaseNamingConvention()属于 Npgsql 的 provider 配置不是 DbContext 的通用配置所以需要放在UseNpgsql的 provider 回调参数里而不是直接链在UseNpgsql外面。3.3 执行迁移命令并检查生成的 SQL接下来生成迁移并更新数据库dotnet ef migrations add InitialCreate dotnet ef database update打开Migrations目录下的迁移文件你会看到表名、列名、约束名全部变成了 snake_casemigrationBuilder.CreateTable( name: blogs, columns: table new { id table.Columnint(type: integer, nullable: false) .Annotation(Npgsql:ValueGenerationStrategy, NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), title table.Columnstring(type: text, nullable: false), summary table.Columnstring(type: text, nullable: false), created_at table.ColumnDateTime(type: timestamp with time zone, nullable: false), author_id table.Columnint(type: integer, nullable: false) }, constraints: table { table.PrimaryKey(pk_blogs, x x.id); table.ForeignKey( name: fk_blogs_authors_author_id, column: x x.author_id, principalTable: authors, principalColumn: id, onDelete: ReferentialAction.Cascade); });注意两个细节主键约束名变成了pk_blogs外键约束名变成了fk_blogs_authors_author_id。Npgsql 内置策略把前缀PK_和FK_也一并转成了小写。这个行为对部分团队来说可能有点意外因为很多 SQL 规范要求约束名用大写前缀。后面我会专门讲这个问题。索引名也会受影响。比如给AuthorId加一个普通索引迁移文件里是这样migrationBuilder.CreateIndex( name: ix_blogs_author_id, table: blogs, column: author_id);原来 EF Core 默认的索引名是IX_blogs_AuthorId经过 snake_case 转换后变成ix_blogs_author_id。表名和列名全部变成了 PostgreSQL 生态最常见的形式直接在 pgAdmin 里看表结构非常干净手写 SQL 也不需要再加双引号了SELECT id, title, created_at FROM blogs WHERE author_id 1;这里再强调一次实体类名是Blog、Author属性名是CreatedAt、FullName一点都没变。CodeFirst 项目里代码侧和数据库侧的命名彻底解耦这也是标题里说的类名仍使用驼峰命名的关键。4. 自定义约定实现不依赖 Npgsql 也能做且更可控4.1 自己写 SnakeCase 转换器的注意事项虽然 Npgsql 内置策略已经很好用但有些团队可能用的是 SQL Server或者想在所有关系数据库上保持同一套命名策略又或者想对转换逻辑做精细化控制。这时候就需要自己实现约定。转换器本身看起来简单但容易写错。比如OrderID这种连续大写缩写的场景一个简单的Regex.Replace(([a-z0-9])([A-Z]), $1_$2)会把APIKey转成apikey而不是api_key因为API和Key之间没有小写字母夹在中间正则匹配不到。更稳妥的手写逻辑是要判断当前位置的前一个字符和后一个字符internal static string ToSnakeCase(string input) { if (string.IsNullOrEmpty(input)) return input; var sb new StringBuilder(input.Length 8); for (var i 0; i input.Length; i) { var c input[i]; if (char.IsUpper(c)) { if (i 0 ShouldSeparate(input, i)) { sb.Append(_); } sb.Append(char.ToLowerInvariant(c)); } else { sb.Append(c); } } return sb.ToString(); } private static bool ShouldSeparate(string input, int index) { var prev input[index - 1]; var next index 1 input.Length ? input[index 1] : \0; return char.IsLower(prev) || char.IsDigit(prev) || (char.IsUpper(prev) char.IsLower(next)); }这个转换器的思路是遇到大写字母时如果前一个字符是小写字母或者数字说明这里是一个单词边界需要插入下划线如果前一个字符是大写字母但后一个字符是小写字母说明当前大写字母是一个新单词的开头也要插入下划线。比如OrderID会正确转成order_idHTTPServer会转成http_server。实际项目里属性名绝大多数是常规 PascalCase这个转换器已经覆盖得足够好。4.2 基于 IModelFinalizingConvention 的完整实现有了转换器接下来实现IModelFinalizingConventionusing System.Text; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Conventions; using Microsoft.EntityFrameworkCore.Metadata.Builders; public class SnakeCaseConvention : IModelFinalizingConvention { public void ProcessModelFinalizing( IConventionModelBuilder modelBuilder, IConventionContextIConventionModelBuilder context) { foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()) { // 继承体系下只处理基类表名避免重复转换 if (entityType.BaseType is not null) continue; var tableName entityType.GetTableName(); if (!string.IsNullOrEmpty(tableName) !HasExplicitTableName(entityType)) { entityType.SetTableName(ToSnakeCase(tableName)); } foreach (var property in entityType.GetDeclaredProperties()) { var columnName property.GetColumnName(); if (!string.IsNullOrEmpty(columnName) !HasExplicitColumnName(property)) { property.SetColumnName(ToSnakeCase(columnName)); } } } } private static bool HasExplicitTableName(IConventionEntityType entityType) { return entityType.FindAnnotation(RelationalAnnotationNames.TableName)? .GetConfigurationSource() ConfigurationSource.Explicit; } private static bool HasExplicitColumnName(IConventionProperty property) { return property.FindAnnotation(RelationalAnnotationNames.ColumnName)? .GetConfigurationSource() ConfigurationSource.Explicit; } }这段代码里有几个点需要解释。第一为什么要检查HasExplicitTableName和HasExplicitColumnName。因为约定不应该覆盖开发人员通过[Table(business_log)]、[Column(custom_name)]显式指定的数据库名称。如果开发人员已经显式命名再转换一次就是画蛇添足。判断方式是通过注解的ConfigurationSource是否来自 Explicit 来决定。第二为什么要用GetDeclaredProperties()而不是GetProperties()。因为 EF Core 的继承映射中基类属性和派生类属性可能会被合并处理如果在每个实体类型上都遍历所有属性很可能出现同一个属性被重复转换的情况。GetDeclaredProperties()只返回当前类型自己声明的属性避免重复处理。第三为什么这里只处理了表名和列名没有处理索引名和外键名。为了让示例代码保持简洁。如果你想彻底掌握命名可以继续遍历entityType.GetIndexes()和entityType.GetForeignKeys()用同样的ToSnakeCase逻辑改写它们的 Name。建议按项目实际需求来裁剪。4.3 注册自定义约定并处理显式指定名称的场景实现类写好后在 DbContext 里注册protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { configurationBuilder.Conventions.Add(_ new SnakeCaseConvention()); }ConfigureConventions是 EF Core 专门留给开发者扩展约定的入口。注意不要在项目里同时使用UseSnakeCaseNamingConvention()和自定义SnakeCaseConvention否则会出现重复转换。比如表名Blogs被内置策略转成blogs自定义约定再处理一次还是blogs看起来结果一样但如果你后续在自定义约定里加了其他逻辑就会变得很难排查。选择一个方案走到底。实际用下来自定义约定最常用的场景就是只转换一部分实体。比如有些表是给第三方系统用的对方要求保持驼峰命名有些表是内部核心表必须用 snake_case。这种情况在约定里添加过滤条件即可var shouldConvert entityType.ClrType.GetCustomAttributes(typeof(SnakeCaseTableAttribute), false).Length 0;或者更简单粗暴一些直接用命名空间前缀过滤命名空间以Domain.Entities开头的实体才参与转换。这种控制粒度是 Npgsql 内置策略做不到的。5. 你大概率会踩的坑都在这里5.1 索引名、外键名也被转换前缀大小写别惊讶前面迁移文件里的例子已经展示了Npgsql 内置策略转换的范围涉及所有数据库标识符不只是表名和列名。索引名IX_blogs_AuthorId变成ix_blogs_author_id主键约束名PK_blogs变成pk_blogs外键约束名FK_blogs_Authors_AuthorId变成fk_blogs_authors_author_id。这对 PostgreSQL 本身没影响因为 PG 对约束名和索引名的解析同样遵循大小写折叠规则。但如果团队内部 DBA 给了一堆数据库设计规范明确要求索引名必须以IX_开头、主键约束名必须以PK_开头而且是严格大写那么内置策略就不完全满足要求了。这时候有两个选择一是接受全部小写的约定名写规范的时候直接按转换后的格式定义二是走自定义约定在处理索引名时把IX前缀保留大写其余部分转 snake_case。我个人推荐第一个方案因为完全没有必要为了前缀大小写多维护一套自定义代码。只要整个团队在同一个约定上对齐小写前缀使用起来没有任何问题。5.2 已有数据表不要直接开 SnakeCase迁移会生成一堆重命名这是最容易翻车的场景尤其是项目已经上线、数据库里已经有真实数据的情况。假设你已经按照 EF Core 默认规则生成了Blogs表和CreatedAt列此时再打开UseSnakeCaseNamingConvention()重新生成一个迁移EF Core 会认为你要把Blogs重命名为blogs把CreatedAt重命名为created_at。迁移会变成这样migrationBuilder.RenameTable( name: Blogs, newName: blogs); migrationBuilder.RenameColumn( name: CreatedAt, table: blogs, newName: created_at);如果你只是改了个命名策略没有真实业务上的重命名需求那这种大范围重命名迁移会非常危险。涉及到外键引用、索引依赖、报表脚本、缓存表结构的地方一旦某个环节没同步更新线上就炸了。我踩过一次明明只是想统一命名风格结果因为一个索引依赖没有一起处理导致某个查询直接报字段找不到。所以我的建议是命名策略必须在第一次生成迁移之前就定好项目初期就加上。如果项目已经跑了很久数据库结构已经很稳定与其通过 EF 迁移去改名不如先评估业务影响用数据库原生脚本在维护窗口期批量重命名然后清掉旧迁移重新建基线。总之不要让 EF Core 自动生成的重命名迁移成为默认方案。5.3 ExecuteSqlRaw 和手写 SQL 不受命名策略保护命名策略只影响 EF Core 自己生成的 SQL。如果你在代码里用了ExecuteSqlRaw、FromSqlRaw、FromSqlInterpolated这类裸 SQL 查询EF Core 不会帮你做任何列名转换。比如var blogs await _context.Blogs .FromSqlRaw(SELECT id, title, created_at FROM blogs WHERE author_id {0}, authorId) .ToListAsync();这条 SQL 里的表名和列名必须自己写成实际的 snake_case因为 EF Core 不解析裸 SQL 内容。如果写成FromSqlRaw(SELECT Id, Title FROM Blogs ...)PostgreSQL 会直接把Id折叠成id然后报column id does not exist看起来和数据库字段对不上实际上只是你没加引号。更隐蔽的问题是如果用了FromSqlRaw参与 LINQ 组合后面又接了一个 EF Core 生成的查询片段两个片段的列名风格不一致就会拼出奇怪的 SQL。所以项目中如果大量手写 SQL建议建立规范所有裸 SQL 里的表名字段名统一使用数据库实际名称。5.4 迁移历史表与命名策略无关别在这上面浪费时间__EFMigrationsHistory是 EF Core 自己用的迁移记录表存放的是每次执行的迁移标识和对应的版本信息。这张表的表名是写死的不管你怎么改命名策略它都叫__EFMigrationsHistory。有人问过要不要改这张表的名字我的答案是不要。这张表是 EF Core 迁移机制的一部分改了之后 EF Core 无法定位自己的迁移记录后续dotnet ef database update会直接不认账。命名策略的目标是业务表不是框架内部表。5.5 显式指定名称优先不是所有名字都会被转用[Table(orders_history)]或者[Column(custom_title)]显式指定的名称Npgsql 内置策略不会覆盖。这一点很多人容易忽略以为打开UseSnakeCaseNamingConvention()之后所有表都会强制变成小写。实际上转换只发生在没有显式名称覆盖的情况下。如果你在实体类上用了特性那就以特性的值为准。这也是合理的开发者手动指定了名称说明有特定的数据库兼容需求约定不应该僭越显式配置。在我上面提到的自定义约定实现里也通过ConfigurationSource.Explicit判断确认了这一点。所以当你发现某个表没有变成 snake_case 的时候先检查是不是有[Table]或[Column]在起作用而不要一头扎进命名策略代码里找 bug。5.6 Npgsql 版本和 EF Core 版本要匹配UseSnakeCaseNamingConvention()从 Npgsql.EntityFrameworkCore.PostgreSQL 6.0 开始提供。如果你的 EF Core 是 6.0对应安装 6.0.x 的 Npgsql 包EF Core 8.0 就安装 8.0.x 的包。版本不匹配最常见的问题不是编译报错而是运行期行为异常模型构建时提示找不到扩展方法或者约定没有生效。这种问题排查起来很烦人所以记住一个原则Npgsql.EntityFrameworkCore.PostgreSQL 的主版本号必须和 EF Core 主版本号一致。6. 进一步思考可以做得更细6.1 部分实体不转换的 Whitelist 方案前面提到自定义约定可以按类型过滤决定是否转换。实际项目中我遇到过这样的需求同一套 EF Core 模型大部分表用 snake_case但有几张表是给老系统对接的老系统要求必须保持CustomerInfo这种驼峰表名。直接在实体上放一个自定义 Attribute 是最清晰的方案[AttributeUsage(AttributeTargets.Class, AllowMultiple false)] public class KeepOriginalNamingAttribute : Attribute { }然后在约定里判断if (entityType.ClrType.GetCustomAttributeKeepOriginalNamingAttribute() ! null) continue;这个方案比维护一个静态表名白名单要直观得多。新同事看到实体类上的特性一眼就知道这张表不参与默认命名转换。6.2 把命名策略写进团队脚手架模板命名策略这种基础设施最好在团队的项目模板里直接预置好。新建项目时脚手架自动带上 Npgsql 8.0 包和UseSnakeCaseNamingConvention()比写在 Wiki 里等待有人想起要高效得多。我在公司里做过一次统一治理把原本散落在各个仓库里的命名实现统一成一个共享的EntityFrameworkCore.Conventions.SnakeCase类库所有新建项目直接引用旧项目批量升级。这样团队内部不会出现这个项目用 snake_case、那个项目用默认驼峰的分裂状态。6.3 如果项目同时支持 PostgreSQL 和 MySQL考虑自定义约定如果项目是那种多数据库适配的产品Npgsql 内置策略就绑死了 PostgreSQL。想让 MySQL 也用 snake_caseSQL Server 也想统一那就必须用自定义约定。前面实现的SnakeCaseConvention并不依赖任何特定数据库 provider它操作的是 EF Core 抽象模型层的表名和列名注解对UseSqlServer、UseMySql、UseSqlite同样生效。跨数据库场景下自定义约定是比 Npgsql 自带策略更通用的解法。代价就是需要自己维护转换器、处理显式名称覆盖、索引约束名等多个边界情况比内置方案多写一些代码。不过这些代码写一次放在共享库往后所有项目都能受益。6.4 迁移文件审查要纳入 CR 流程开启命名策略之后第一次生成的迁移文件会和之前默认风格差异很大。建议把迁移文件当成正式的代码提交来审查逐项核对表名、列名、索引名、外键名是否符合预期。很多命名问题在迁移阶段就能发现不用等到数据库建完再去补救。我一般会打开生成的 SQL 脚本再快速扫一遍dotnet ef migrations script -o init.sql看一看init.sql里有没有异常命名比如某个列没有被转换、某个约束名还是默认驼峰。确认无误后再提交可以省掉后续至少一轮改表结构的折腾。最后再说几句实在话EF Core CodeFirst 做 PostgreSQL 蛇形命名这件事技术含量不算高Npgsql 内置策略能覆盖绝大多数场景。真正麻烦的是团队规范的一致性和项目历史包袱。我现在做新项目的时候第一行 DbContext 配置就会把UseSnakeCaseNamingConvention()加上然后告诉所有开发人员C# 代码里正常写 PascalCase数据库层你不用管。这样就够了。如果你正在折腾一个已经有一堆旧迁移的项目我的建议很直接把这篇文章里提到的坑过一遍然后开一个分支单独跑一次迁移把生成的 SQL 逐个核对完再谈合并。命名策略这种事晚定不如早定早定比晚定省心太多。
网站建设高端定制企业官网