EF Core DetectChanges调用顺序不同时导航集合残留已删除实体问题
EF Core多对多导航属性删除不同步问题
问题现象
在显式连接实体的多对多关系场景中,从post.PostTags集合移除连接实体PostTag后,预期被移除的实体会从对应tag.PostTags集合中同步消失,实际行为受方法调用顺序影响:
- 调用
ctx.ChangeTracker.DetectChanges()之前查询Tag集合,最终tag.PostTags集合会正确移除已删除的连接实体,符合预期 - 查询Tag集合前先调用
ctx.ChangeTracker.DetectChanges(),id=2的tag实体导航属性PostTags中会保留状态为Deleted的{PostId: 1, TagId: 2}连接实体,即便再次调用ctx.ChangeTracker.DetectChanges(),该已删除实体也不会从集合中消失。
复现代码
实体定义
using System.Collections.Generic; namespace Web.Tests { public class Post { public int Id { get; set; } public string Title { get; set; } public string Content { get; set; } public IList<PostTag> PostTags { get; } = new List<PostTag>(); // 集合导航属性 } public class Tag { public int Id { get; set; } public string Text { get; set; } public IList<PostTag> PostTags { get; } = new List<PostTag>(); // 集合导航属性 } public class PostTag { public int PostId { get; set; } // 复合主键第一部分,Post外键 public int TagId { get; set; } // 复合主键第二部分,Tag外键 public Post Post { get; set; } // 引用导航属性 public Tag Tag { get; set; } // 引用导航属性 } }
DbContext配置
using Microsoft.EntityFrameworkCore; namespace Web.Tests { public class BlogContext : DbContext { public BlogContext(DbContextOptions<BlogContext> options) : base(options) { } public DbSet<Post> Posts { get; set; } public DbSet<Tag> Tags { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Post>() .HasData( new Post { Id = 1, Title = "How to use vs?", Content = "blababa" } ); modelBuilder.Entity<Tag>() .HasData( new Tag { Id = 1, Text = ".NET" }, new Tag { Id = 2, Text = "C#" } ); modelBuilder.Entity<PostTag>() .HasKey(x => new { x.PostId, x.TagId }); modelBuilder.Entity<PostTag>() .HasOne(x => x.Post) .WithMany(x => x.PostTags); modelBuilder.Entity<PostTag>() .HasOne(x => x.Tag) .WithMany(x => x.PostTags); modelBuilder.Entity<PostTag>() .HasData( new PostTag { PostId = 1, TagId = 1 }, new PostTag { PostId = 1, TagId = 2 } ); base.OnModelCreating(modelBuilder); } } }
异常场景测试用例
[Fact] public void Test1_Abnormal() { var ctx = CreateContext(); var post = ctx.Posts.Include(x => x.PostTags).SingleOrDefault(x => x.Id == 1); if (post != null) { post.PostTags.RemoveAt(1); } ctx.ChangeTracker.DetectChanges(); Debug.WriteLine(ctx.ChangeTracker.DebugView.LongView); var tag = ctx.Tags.Include(x => x.PostTags).ToList(); Debug.WriteLine(ctx.ChangeTracker.DebugView.LongView); ctx.ChangeTracker.DetectChanges(); Debug.WriteLine(ctx.ChangeTracker.DebugView.LongView); ctx.SaveChanges(); }
各阶段变更跟踪器输出:
- 第一次DetectChanges后:
Post {Id: 1} Unchanged Id: 1 PK Content: 'blababa' Title: 'How to use vs?' PostTags: [{PostId: 1, TagId: 1}] PostTag {PostId: 1, TagId: 1} Unchanged PostId: 1 PK FK TagId: 1 PK FK Post: {Id: 1} Tag: <null> PostTag {PostId: 1, TagId: 2} Deleted PostId: 1 PK FK TagId: 2 PK FK Post: <null> Tag: <null>
- 查询Tag集合后:
Post {Id: 1} Unchanged Id: 1 PK Content: 'blababa' Title: 'How to use vs?' PostTags: [{PostId: 1, TagId: 1}] PostTag {PostId: 1, TagId: 1} Unchanged PostId: 1 PK FK TagId: 1 PK FK Post: {Id: 1} Tag: {Id: 1} PostTag {PostId: 1, TagId: 2} Deleted PostId: 1 PK FK TagId: 2 PK FK Post: <null> Tag: {Id: 2} Tag {Id: 1} Unchanged Id: 1 PK Text: '.NET' PostTags: [{PostId: 1, TagId: 1}] Tag {Id: 2} Unchanged Id: 2 PK Text: 'C#' PostTags: [{PostId: 1, TagId: 2}]
- 第二次DetectChanges后(异常:已删除的PostTag仍存在于Tag导航属性中):
Post {Id: 1} Unchanged Id: 1 PK Content: 'blababa' Title: 'How to use vs?' PostTags: [{PostId: 1, TagId: 1}] PostTag {PostId: 1, TagId: 1} Unchanged PostId: 1 PK FK TagId: 1 PK FK Post: {Id: 1} Tag: {Id: 1} PostTag {PostId: 1, TagId: 2} Deleted PostId: 1 PK FK TagId: 2 PK FK Post: <null> Tag: {Id: 2} Tag {Id: 1} Unchanged Id: 1 PK Text: '.NET' PostTags: [{PostId: 1, TagId: 1}] Tag {Id: 2} Unchanged Id: 2 PK Text: 'C#' PostTags: [{PostId: 1, TagId: 2}]
正常场景测试用例
[Fact] public void Test1_Normal() { var ctx = CreateContext(); var post = ctx.Posts.Include(x => x.PostTags).SingleOrDefault(x => x.Id == 1); if (post != null) { post.PostTags.RemoveAt(1); } var tag = ctx.Tags.Include(x => x.PostTags).ToList(); Debug.WriteLine(ctx.ChangeTracker.DebugView.LongView); ctx.ChangeTracker.DetectChanges(); Debug.WriteLine(ctx.ChangeTracker.DebugView.LongView); ctx.SaveChanges(); }
各阶段变更跟踪器输出:
- 查询Tag集合后:
Post {Id: 1} Unchanged Id: 1 PK Content: 'blababa' Title: 'How to use vs?' PostTags: [{PostId: 1, TagId: 1}] PostTag {PostId: 1, TagId: 1} Unchanged PostId: 1 PK FK TagId: 1 PK FK Post: {Id: 1} Tag: {Id: 1} PostTag {PostId: 1, TagId: 2} Unchanged PostId: 1 PK FK TagId: 2 PK FK Post: {Id: 1} Tag: {Id: 2} Tag {Id: 1} Unchanged Id: 1 PK Text: '.NET' PostTags: [{PostId: 1, TagId: 1}] Tag {Id: 2} Unchanged Id: 2 PK Text: 'C#' PostTags: [{PostId: 1, TagId: 2}]
- 调用DetectChanges后(符合预期:id=2的Tag的PostTags集合为空):
Post {Id: 1} Unchanged Id: 1 PK Content: 'blababa' Title: 'How to use vs?' PostTags: [{PostId: 1, TagId: 1}] PostTag {PostId: 1, TagId: 1} Unchanged PostId: 1 PK FK TagId: 1 PK FK Post: {Id: 1} Tag: {Id: 1} PostTag {PostId: 1, TagId: 2} Deleted PostId: 1 PK FK TagId: 2 PK FK Post: <null> Tag: {Id: 2} Tag {Id: 1} Unchanged Id: 1 PK Text: '.NET' PostTags: [{PostId: 1, TagId: 1}] Tag {Id: 2} Unchanged Id: 2 PK Text: 'C#' PostTags: []
原因
这是EF Core变更跟踪的设计行为:当先调用DetectChanges()将连接实体标记为Deleted后,再通过查询加载关联的Tag实体时,EF Core会将已经标记为删除的连接实体回填到Tag的PostTags集合中。后续DetectChanges()不会自动从导航集合中移除已标记为Deleted的实体,避免开发人员遍历集合处理删除逻辑时出现集合被意外修改的异常。
解决方案
任选以下一种方案即可保证行为不受调用顺序影响:
- 方案1:显式清理已删除的连接实体
操作完实体关系、调用DetectChanges()后,手动遍历变更跟踪器中所有状态为Deleted的连接实体,将其从两端导航集合中移除:ctx.ChangeTracker.DetectChanges(); var deletedJoinEntries = ctx.ChangeTracker.Entries<PostTag>() .Where(e => e.State == EntityState.Deleted) .ToList(); foreach (var entry in deletedJoinEntries) { var joinEntity = entry.Entity; // 从Post的导航集合移除 if (joinEntity.Post != null) { ((List<PostTag>)joinEntity.Post.PostTags).Remove(joinEntity); } // 从Tag的导航集合移除 if (joinEntity.Tag != null) { ((List<PostTag>)joinEntity.Tag.PostTags).Remove(joinEntity); } } - 方案2:使用隐式连接实体配置多对多关系
如果不需要为连接实体扩展额外字段,不要显式定义PostTag实体,直接配置Post和Tag的直接多对多关系,EF Core会自动维护隐式连接实体的关系修复,不会出现已删除实体残留问题:// 实体定义调整 public class Post { public int Id { get; set; } public string Title { get; set; } public string Content { get; set; } public IList<Tag> Tags { get; } = new List<Tag>(); } public class Tag { public int Id { get; set; } public string Text { get; set; } public IList<Post> Posts { get; } = new List<Post>(); } // OnModelCreating配置调整 modelBuilder.Entity<Post>() .HasMany(p => p.Tags) .WithMany(t => t.Posts) .UsingEntity(j => j.HasData( new { PostsId = 1, TagsId = 1 }, new { PostsId = 1, TagsId = 2 } ));
内容的提问来源于stack exchange,提问作者qslcna
相关产品推荐
相关产品推荐

