升级.NET6时EF Core字符串主键实体自动生成TempId1影子键问题
问题背景
在将项目从.NET Core 3.1升级至.NET 6的过程中,实体字符串主键配置出现异常,复现场景如下:
基础Country实体定义:
public class Country { public string CountryCode { get; set; } public string Name { get; set; } }
已通过FluentAPI将CountryCode配置为实体主键,同时调用ValueGeneratedNever()禁用主键值自动生成,配置代码:
modelBuilder.Entity<Country>().HasKey(c => c.CountryCode); modelBuilder.Entity<Country>().Property(c => c.CountryCode).ValueGeneratedNever();
Country实体与其他实体存在关联,以Company实体为例:
public class Company { public int Id { get; set; } public string CompanyCode { get; set; } public string Name { get; set; } public string CountryCode { get; set; } public Country Country { get; set; } }
二者关联关系配置:
modelBuilder.Entity<Company>().HasOne<Country>(c => c.Country).WithMany().HasForeignKey(c => c.CountryCode);
以上配置在.NET Core 3.1环境下运行正常,升级到.NET 6后生成迁移时,Country实体对应表会自动创建名为TempId1的影子键(shadow key),迁移生成的建表代码如下:
migrationBuilder.CreateTable( name: "Countries", columns: table => new { CountryCode = table.Column<string>(type: "nvarchar(450)", nullable: false), Name = table.Column<string>(type: "nvarchar(max)", nullable: true), TempId1 = table.Column<int>(type: "int", nullable: false) }, constraints: table => { table.PrimaryKey("PK_Countries", x => x.CountryCode); table.UniqueConstraint("AK_Countries_TempId1", x => x.TempId1); });
已知问题与C# 8.0及以上版本的可为空引用类型(Nullable Reference Type, NRT)功能相关,尝试给CountryCode属性添加= null!初始化向EF Core声明主键非空,仍无法阻止影子键自动生成。
问题原因
.NET 6配套的EF Core 6.0对NRT的默认处理逻辑和旧版本存在核心差异:
- 当项目启用可为空引用类型时,EF Core会默认将无
?修饰的引用类型属性识别为非空、有?修饰的识别为可空,但该识别逻辑存在明确边界:如果引用类型属性没有在构造函数中初始化、也没有通过配置明确告知EF Core运行时一定会赋值,EF Core的模型约定会判定该属性无法作为可靠的主键/外键匹配依据。 - 之前使用的
= null!仅用于跳过编译器的空检查,不会改变EF Core的模型约定判定逻辑——EF Core会识别到该属性实际初始值为null,判定运行时存在空值风险,因此会按照内部约定自动生成一个int类型的影子备用键TempId1用来做关联追踪。 - 该场景下
Country实体是关联的主表端,如果反向导航属性未配置、同时EF Core判定手动配置的主键不符合"非空且可靠"的约定要求,就会自动生成备用影子键供外键关联使用。
解决方案
按优先级选择以下任意一种方式即可解决:
- 最推荐:从实体定义层面明确主键非空,彻底消除EF Core的判定歧义
不需要使用null!,直接给主键属性设置EF Core可识别的非空默认值(实际业务插入数据时会被业务值覆盖):public class Country { // 初始化空字符串后,EF Core可直接识别为非空可靠主键 public string CountryCode { get; set; } = string.Empty; public string Name { get; set; } } - 全局关闭EF Core的NRT默认约定处理(适合旧项目快速升级、无需大量修改实体的场景)
在DbContext的OnConfiguring方法中添加配置:protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(你的数据库连接字符串) // 关闭EF Core基于可为空引用类型的自动模型约定判定 .UseNullableReferenceTypes(false); } - 单独给主键配置强制非空规则,绕过默认约定判定
如果不想修改实体定义、也不想全局关闭NRT处理,可以在FluentAPI配置中为主键追加IsRequired()配置,明确告知EF Core该字段非空:modelBuilder.Entity<Country>().Property(c => c.CountryCode).IsRequired().ValueGeneratedNever();
注意:修复配置后需要先删除之前生成的包含
TempId1字段的错误迁移文件,再重新生成迁移,才不会出现多余的影子键字段。
内容的提问来源于stack exchange,提问作者Superman.Lopez
相关产品推荐
相关产品推荐

