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

EF Core销毁并重建DbContext后无法加载子实体的问题求助

EF Core 新建DbContext后子实体无法加载问题排查

问题现象

使用EF Core开发时,遵循DbContext短生命周期原则,每次事务注入新的DbContext实例。但销毁重建DbContext后,查询父实体Machine时,子实体StatusChanges始终为空,尽管数据库中存在关联数据且外键匹配。

第一次创建仓储时,添加Machine及其子实体MachineStatusChange后,在当前DbContext中能正常查询到包含子实体的数据;但第二次创建新仓储(对应新DbContext)后,仅能查询到父实体,子实体为空。

示例代码

public InitialTestData(Func<IMachineRepository> machineRepositoryFactory)
{
    {
        using var machineRepository = machineRepositoryFactory();
    
        if (!machineRepository.FindAll().Any())
        {
            machineRepository.Add("411-01", "https://localhost:5002/411-01");
            machineRepository.Add("411-02", "https://localhost:5002/411-02");
            machineRepository.Add("411-03", "https://localhost:5002/411-03");
            machineRepository.Add("413-01", "https://localhost:5002/413-01");
            machineRepository.Add("413-02", "https://localhost:5002/413-02");
            machineRepository.Add("413-03", "https://localhost:5002/413-03");
            machineRepository.Save();

            foreach (var machineEntity in machineRepository.FindAll())
            {
                var machineStatusChangeEntity = new MachineStatusChange
                {
                    DateTime = DateTime.Now, 
                    State = MachineStateDataModel.Idle
                };
                machineEntity.StatusChanges.Add(machineStatusChangeEntity);
                machineRepository.Save();
            }
        }

        var bla = machineRepository.FindAll().ToList();
        foreach (var machine in bla)
        {
            // 此处能正常获取到1条子实体
        }
    }

    {
        using var machineRepository = machineRepositoryFactory();

        var bla = machineRepository.FindAll().ToList();
        foreach (var machine in bla)
        {
            // 此处子实体StatusChanges为空,但数据库中存在对应数据
        }
    }
}

相关代码

仓储模式实现

public class MachineRepository : RepositoryBase<Machine>, IMachineRepository
{
    public MachineRepository(PatDatabase repositoryContext) : base(repositoryContext)
    {
    }       
}

public abstract class RepositoryBase<T> : IRepository<T> where T : class 
{
    protected PatDatabase RepositoryContext { get; set; }

    protected RepositoryBase(PatDatabase repositoryContext) 
    {
        RepositoryContext = repositoryContext; 
    }
    
    public IQueryable<T> FindAll() => RepositoryContext.Set<T>();

    public IQueryable<T> FindByCondition(Expression<Func<T, bool>> expression) =>
        RepositoryContext.Set<T>().Where(expression);

    public EntityEntry<T> Refresh(T entity) => RepositoryContext.Entry(entity);

    public void Create(T entity) => RepositoryContext.Set<T>().Add(entity);
    public void Update(T entity) => RepositoryContext.Set<T>().Update(entity);
    public void Delete(T entity) => RepositoryContext.Set<T>().Remove(entity);        
}

实体类定义

[Table("Machine")]
public class Machine
{
    [Key]
    public Guid Id { get; set; }
    
    public string Name { get; set; }

    public string BaseUrl { get; set; }

    public ICollection<MachineStatusChange> StatusChanges { get; set; } = new List<MachineStatusChange>();
}

[Table("MachineStatusChange")]
public record MachineStatusChange
{
    [Key]
    public Guid Id { get; set; }
    
    [ForeignKey(nameof(Machine))]
    public Guid MachineId { get; set; }

    public DateTime DateTime { get; set; }

    public MachineStateDataModel State { get; set; }
    
    public Machine Machine { get; set; }
}

DbContext实现

public class PatDatabase : DbContext
{
    private readonly string _connectionString;

    // TODO, discuss/investigate
    // Any purpose to define DbSet<T> as they did in tutorial?
    //public DbSet<Machine> Machines { get; set; }

    /// <summary>
    /// Used by dotnet ef migrations
    /// NOTE: Autofac will always prefer the constructor with most arguments
    /// </summary>
    public PatDatabase()
    {
        var assembly = Assembly.GetExecutingAssembly();
        using var stream = assembly.GetManifestResourceStream("IAI.ToolSuite.PAT.Server.appsettings.json");
        
        var configuration = new ConfigurationBuilder().AddJsonStream(stream).Build();
        
        // TODO BP: replace "LocalMySqlConnection" with "MySqlConnection" when figured out migrations in docker properly
        // It seems that 'dotnet ef migrations remove' requires access to the database(??)
        // In my case, my docker-compose is down, I would expect that's not a problem.... To be investigated... 
        _connectionString = configuration["MySqlConnection:connectionString"];
    }
    
    /// <summary>
    /// Constructor used by autofac (autofac will always prefer constructor with most arguments if resolvable)
    /// </summary>
    public PatDatabase(IConfiguration configuration, IWebHostEnvironment env)
    {
        if (env.IsLocal())
        {
            _connectionString = configuration["MySqlConnection:connectionString"];
        }
        else
        {
            _connectionString = configuration["MySqlConnection:connectionString"];
        }
    }
    
    /// <summary>
    /// IMPORTANT
    /// override the OnConfiguring allows us to keep a parameterless constructor
    /// ef migrations tool requires a parameterless constructor
    /// </summary>
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseMySql(_connectionString, MySqlServerVersion.LatestSupportedServerVersion,
            builder =>
            {
                builder.EnableRetryOnFailure(20, TimeSpan.FromSeconds(10), null);
            });
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Machine>().HasMany(m => m.StatusChanges);
        modelBuilder.Entity<MachineStatusChange>()
            .HasOne(m => m.Machine)
            .WithMany(m => m.StatusChanges);        
    }

    public void MigrateIfNeeded()
    {
        if (Database.GetPendingMigrations().Any())
        {
            Database.Migrate();
        }

        // I think calling ensure created before migrations can cause issues
        // when migrations exist but the database or tables were dropped
        Database.EnsureCreated();
    }
}

问题原因分析

  1. 延迟加载未启用:EF Core默认关闭延迟加载。第一次查询时,实体处于当前DbContext的跟踪状态,访问子实体时直接从内存缓存获取;但第二次使用新DbContext时,查询的实体未被跟踪,且未触发延迟加载,导致子实体为空。
  2. 导航属性未标记为virtual:即使启用延迟加载,EF Core需要通过动态代理实现延迟加载,要求导航属性必须是virtual类型,当前实体的导航属性未满足此条件。
  3. MachineStatusChange使用record类型:EF Core对record类型的延迟加载支持有限,record的不可变语义与动态代理生成存在冲突,可能导致延迟加载失效。
  4. 未显式使用急切加载:查询父实体时未通过Include()方法关联子实体,导致查询仅返回父实体数据,未加载子实体。

解决方法

方法一:启用延迟加载并修正导航属性

  1. 在DbContext的OnConfiguring方法中启用延迟加载代理:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.UseMySql(_connectionString, MySqlServerVersion.LatestSupportedServerVersion,
            builder =>
            {
                builder.EnableRetryOnFailure(20, TimeSpan.FromSeconds(10), null);
            })
        .UseLazyLoadingProxies(); // 添加此行启用延迟加载
}
  1. 将实体的导航属性标记为virtual,并建议将record改为class以避免冲突:
[Table("Machine")]
public class Machine
{
    // ... 其他属性
    public virtual ICollection<MachineStatusChange> StatusChanges { get; set; } = new List<MachineStatusChange>();
}

[Table("MachineStatusChange")]
public class MachineStatusChange
{
    // ... 其他属性
    public virtual Machine Machine { get; set; }
}

方法二:使用急切加载(显式Include子实体)

修改MachineRepository,添加包含子实体的查询方法:

public class MachineRepository : RepositoryBase<Machine>, IMachineRepository
{
    public MachineRepository(PatDatabase repositoryContext) : base(repositoryContext)
    {
    }

    public IQueryable<Machine> FindAllWithStatusChanges()
    {
        return RepositoryContext.Set<Machine>().Include(m => m.StatusChanges);
    }
}

查询时调用该方法:

var bla = machineRepository.FindAllWithStatusChanges().ToList();

方法三:优化实体关联配置(可选)

在OnModelCreating中明确配置外键与删除行为,确保关联关系正确:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Machine>()
        .HasMany(m => m.StatusChanges)
        .WithOne(sc => sc.Machine)
        .HasForeignKey(sc => sc.MachineId)
        .OnDelete(DeleteBehavior.Cascade); // 可根据业务需求调整删除行为
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 17:35:15