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(); } }
问题原因分析
- 延迟加载未启用:EF Core默认关闭延迟加载。第一次查询时,实体处于当前DbContext的跟踪状态,访问子实体时直接从内存缓存获取;但第二次使用新DbContext时,查询的实体未被跟踪,且未触发延迟加载,导致子实体为空。
- 导航属性未标记为
virtual:即使启用延迟加载,EF Core需要通过动态代理实现延迟加载,要求导航属性必须是virtual类型,当前实体的导航属性未满足此条件。 MachineStatusChange使用record类型:EF Core对record类型的延迟加载支持有限,record的不可变语义与动态代理生成存在冲突,可能导致延迟加载失效。- 未显式使用急切加载:查询父实体时未通过
Include()方法关联子实体,导致查询仅返回父实体数据,未加载子实体。
解决方法
方法一:启用延迟加载并修正导航属性
- 在DbContext的
OnConfiguring方法中启用延迟加载代理:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseMySql(_connectionString, MySqlServerVersion.LatestSupportedServerVersion, builder => { builder.EnableRetryOnFailure(20, TimeSpan.FromSeconds(10), null); }) .UseLazyLoadingProxies(); // 添加此行启用延迟加载 }
- 将实体的导航属性标记为
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
相关产品推荐
相关产品推荐

