You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

升级.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的默认处理逻辑和旧版本存在核心差异:

  1. 当项目启用可为空引用类型时,EF Core会默认将无?修饰的引用类型属性识别为非空、有?修饰的识别为可空,但该识别逻辑存在明确边界:如果引用类型属性没有在构造函数中初始化、也没有通过配置明确告知EF Core运行时一定会赋值,EF Core的模型约定会判定该属性无法作为可靠的主键/外键匹配依据。
  2. 之前使用的= null!仅用于跳过编译器的空检查,不会改变EF Core的模型约定判定逻辑——EF Core会识别到该属性实际初始值为null,判定运行时存在空值风险,因此会按照内部约定自动生成一个int类型的影子备用键TempId1用来做关联追踪。
  3. 该场景下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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.03 07:18:30