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

