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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 06:45:56