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

EF Core 6迁移后查询含空外键数据触发空值异常求助

问题描述

我正将项目从.Net 4.7迁移至.Net 6,同时迁移Entity Framework。已通过dotnet ef migrations add PortedToEFCore生成首个迁移,并按微软要求清空该迁移类的Up()和Down()方法。
为匹配旧EF的关联表名,我在OnModelCreating中配置了关联表RoleAccounts和AccountAccesses,替换默认的AccountRole和AccessAccount。

我的Account模型中,AccountProfileId和StoreId定义为可空Guid(Guid?),但查询数据库中这两个字段为NULL的ADMIN账户时,执行var xxx = await db.Accounts.ToListAsync();会触发异常:

System.Data.SqlTypes.SqlNullValueException: 'Data is Null. This method or property cannot be called on Null values.'

当为该账户绑定AccountProfile并设置StoreId后,可正常查询。请问为何模型中已声明可选的外键仍触发此异常?如何解决?

模型代码

public class Account : ObjectMapper
{
    [Key]
    [Toolbelt.ComponentModel.DataAnnotations.Schema.V5.IndexColumn(IsUnique = true)]
    public Guid ID { get; set; } = Guid.NewGuid();

    [Required]
    [Toolbelt.ComponentModel.DataAnnotations.Schema.V5.IndexColumn(IsUnique = true)]
    [StringLength(50)]
    [Toolbelt.ComponentModel.DataAnnotations.Schema.V5.IndexColumn("UserStore", 1, IsUnique = true)]
    public string UserName { get; set; }

    [Required]
    public string Password { get; set; }

    public string Hash { get; set; }

    public ICollection<Role> Roles { get; set; }

    [ForeignKey("AccountProfile")]
    public Guid? AccountProfileId { get; set; }

    public AccountProfile AccountProfile { get; set; }

    [ForeignKey("Store")]
    [Toolbelt.ComponentModel.DataAnnotations.Schema.V5.IndexColumn("UserStore", 2, IsUnique = true)]
    public Guid? StoreId { get; set; }
    public Store Store {get;set;}

    public ICollection<Access> Access { get; set; }
}

public class Access
{
    [Key]
    [Toolbelt.ComponentModel.DataAnnotations.Schema.V5.IndexColumn(IsUnique = true)]
    public Guid ID { get; set; } = Guid.NewGuid();

    public string AccessName { get; set; }

    public ICollection<Account> Accounts { get; set; }
}

public class AccountProfile
{
    [Key]
    [Toolbelt.ComponentModel.DataAnnotations.Schema.V5.IndexColumn(IsUnique = true)]
    public Guid ID { get; set; } = Guid.NewGuid();

    [Required]
    public string Name { get; set; }

    public string Description { get; set; }

    public string Phone { get; set; }

    public string Email { get; set; }
    public string Address { get; set; }
    public string City { get; set; }
    public string Province { get; set; }
    public string PostCode { get; set; }
}

public class Role
{
    [Key]
    [Toolbelt.ComponentModel.DataAnnotations.Schema.V5.IndexColumn(IsUnique = true)]
    public Guid ID { get; set; } = Guid.NewGuid();

    [Required]
    [StringLength(50)]
    [Toolbelt.ComponentModel.DataAnnotations.Schema.V5.IndexColumn(IsUnique = true)]
    public string RoleName { get; set; }

    public ICollection<Account> Accounts { get; set; }
}

关联表配置代码

protected override void OnModelCreating(ModelBuilder mb)
{
    mb.BuildIndexesFromAnnotations();
    base.OnModelCreating(mb);

    mb.Entity<Account>()
        .HasMany(left => left.Roles)
        .WithMany(right => right.Accounts)
        .UsingEntity<Dictionary<string, object>>(
        "RoleAccounts",
    j => j
        .HasOne<Role>()
        .WithMany()
        .HasForeignKey("Role_ID"),
    j => j
        .HasOne<Account>()
        .WithMany()
        .HasForeignKey("Account_ID"));

    mb.Entity<Account>()
        .HasMany(left => left.Access)
        .WithMany(right => right.Accounts)
        .UsingEntity<Dictionary<string, object>>(
        "AccountAccesses",
    j => j
        .HasOne<Access>()
        .WithMany()
        .HasForeignKey("Access_ID"),
    j => j
        .HasOne<Account>()
        .WithMany()
        .HasForeignKey("Account_ID"));
}

原因分析

问题出在.NET 6默认启用的Nullable Reference Types(可空引用类型)特性上。虽然你把外键AccountProfileId和StoreId声明为Guid?(可空值类型),但对应的导航属性AccountProfile和Store是普通的引用类型(没有加?)。在NRT模式下,EF Core会默认将这种非可空的导航属性视为必需关联,即使外键是可空的。当数据库中外键为NULL时,EF Core尝试将NULL赋值给非可空的导航属性,就会抛出SqlNullValueException。

解决方案

有两种可行的解决方式,任选其一即可:

方式一:将导航属性声明为可空类型

修改Account模型中的导航属性,在类型后添加?标记为可空引用类型:

public class Account : ObjectMapper
{
    // 其他属性保持不变...

    [ForeignKey("AccountProfile")]
    public Guid? AccountProfileId { get; set; }

    // 添加?标记为可空
    public AccountProfile? AccountProfile { get; set; }

    [ForeignKey("Store")]
    [Toolbelt.ComponentModel.DataAnnotations.Schema.V5.IndexColumn("UserStore", 2, IsUnique = true)]
    public Guid? StoreId { get; set; }
    // 添加?标记为可空
    public Store? Store { get; set; }

    // 其他属性保持不变...
}

方式二:在OnModelCreating中显式配置关联为可选

如果不想修改模型的属性声明,可以在DbContext的OnModelCreating方法中,显式告诉EF Core这两个关联是可选的:

protected override void OnModelCreating(ModelBuilder mb)
{
    mb.BuildIndexesFromAnnotations();
    base.OnModelCreating(mb);

    // 现有关联表配置保持不变...

    // 配置AccountProfile关联为可选
    mb.Entity<Account>()
        .HasOne(a => a.AccountProfile)
        .WithOne() // 如果AccountProfile没有反向导航,就用WithOne()
        .HasForeignKey<Account>(a => a.AccountProfileId)
        .IsRequired(false); // 明确标记为非必需

    // 配置Store关联为可选
    mb.Entity<Account>()
        .HasOne(a => a.Store)
        .WithOne() // 根据实际情况调整反向导航,如果有的话替换为对应的属性
        .HasForeignKey<Account>(a => a.StoreId)
        .IsRequired(false);
}

注意:如果AccountProfile或Store模型中存在指向Account的反向导航属性,需要将WithOne()替换为对应的lambda表达式,比如WithOne(ap => ap.Account)(假设AccountProfile有Account属性)。

内容的提问来源于stack exchange,提问作者Alvin Stefanus

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.24 21:18:17