EF Core使用.Include加载关联属性时父实体属性取值抛出异常问题
实体Bill的TotalQuantity属性依赖虚拟导航成员AllSales,使用EF Core的.Include(b => b.AllSales)加载实体时,加载过程中AllSales为null,触发自定义VirualMemberNotLoadedException;调试发现加载完成后AllSales已正常加载,但加载过程中调用TotalQuantity的getter时AllSales确实为null。启用懒加载后问题依旧,若将异常改为返回0可运行,但无法区分“AllSales未加载”和“实际Quantity总和为0”两种情况。
实体类代码
public class Bill { private int _totalQuantityStored; public static Bill Create() { var bill = new Bill(); InitializeVirtualMembers(bill); } private static void InitializeVirtualMembers(Bill bill) { bill.AllSales = new List<Sales>(); } private Bill() { } [AutoIncrement] public int BillId { get; set; } public int TotalQuantity { get { if (_totalQuantityStored > 0) { return _totalQuantityStored; } if (AllSales is null) { throw VirualMemberNotLoadedException<Sales>(nameof(AllSales)); } return AllSales.Sum(a => a.Quantity); } set => _totalQuantityStored = value; } public virtual IList<Sales> AllSales { get; private set; } }
加载代码
var bill = await context.Bills .Where(b => b.BillId == billId) .Include(b => b.AllSales).SingleAsync();
EF Core的实体加载顺序为:先构造父实体实例,再填充标量属性,最后加载并赋值关联的导航属性。而TotalQuantity的getter可能在导航属性赋值前被触发(比如EF跟踪实体时的内部操作、调试器自动求值、序列化等场景),此时AllSales尚未被初始化或加载,导致null检查抛出异常。
方案1:在构造函数中初始化导航属性为空集合
修改私有构造函数,直接初始化AllSales为空列表,确保它永远不会为null:
private Bill() { AllSales = new List<Sales>(); }
此时加载前AllSales是空集合,Sum结果为0;加载后是实际的Sales数据。若需要区分“未加载”和“实际总和为0”,可通过EF的Entry判断导航属性是否已加载:
public int TotalQuantity { get { if (_totalQuantityStored > 0) { return _totalQuantityStored; } // 通过DbContext判断AllSales是否已加载 using var context = new YourDbContext(); var entry = context.Entry(this); if (!entry.Collection(b => b.AllSales).IsLoaded) { throw VirualMemberNotLoadedException<Sales>(nameof(AllSales)); } return AllSales.Sum(a => a.Quantity); } set => _totalQuantityStored = value; }
注:该方案需要实体能访问DbContext,若要保持实体独立性,可考虑通过依赖注入传递上下文。
方案2:将计算逻辑移至服务层
不在实体内部处理计算,改为在业务服务中实现,避免实体依赖导航属性的加载状态:
public class BillService { private readonly YourDbContext _context; public BillService(YourDbContext context) { _context = context; } public async Task<int> GetTotalQuantityAsync(int billId) { var bill = await _context.Bills .Include(b => b.AllSales) .SingleAsync(b => b.BillId == billId); if (bill._totalQuantityStored > 0) { return bill._totalQuantityStored; } if (!_context.Entry(bill).Collection(b => b.AllSales).IsLoaded) { throw VirualMemberNotLoadedException<Sales>(nameof(bill.AllSales)); } return bill.AllSales.Sum(a => a.Quantity); } }
此时实体的TotalQuantity可改为仅存储_totalQuantityStored,或保留但仅返回存储值,计算逻辑由服务统一处理。
方案3:使用数据库计算列(数据库支持时)
将TotalQuantity设为数据库计算列,由数据库负责计算总和,EF Core直接读取结果:
[DatabaseGenerated(DatabaseGeneratedOption.Computed)] public int TotalQuantity { get; private set; }
在数据库中创建对应计算列(以SQL Server为例):
ALTER TABLE Bills ADD TotalQuantity AS (SELECT SUM(Quantity) FROM Sales WHERE Sales.BillId = Bills.BillId)
这种方式彻底避免了实体内部的计算逻辑,完全由数据库和EF Core处理,不存在加载顺序问题。
内容的提问来源于stack exchange,提问作者Tushar Swarup Tandon

