如何通过分配新的Collection来更新Many-Many中的Collection?

在entity framework核心2.0中, PostCategory之间有很多关系(绑定类是PostCategory )。

当用户更新Post ,整个Post对象(及其PostCategory集合)将被发送到服务器,在这里我想重新分配新收到的Collection PostCategory (用户可以通过添加新类别和删除来显着更改此Collection一些类别)。

我用来更新该集合的简化代码(我只是分配了全新的集合):

 var post = await dbContext.Posts .Include(p => p.PostCategories) .ThenInclude(pc => pc.Category) .SingleOrDefaultAsync(someId); post.PostCategories = ... Some new collection...; // <<< dbContext.Posts.Update(post); await dbContext.SaveChangesAsync(); 

此新集合具有在先前集合中具有相同Id对象的对象(例如,用户删除了一些(但不是全部)类别) 。 因为,我得到一个例外:

System.InvalidOperationException:无法跟踪实体类型“PostCategory”的实例,因为已经跟踪了另一个具有{‘CategoryId’,’PostId’}键值的实例。

如何在不获得此exception的情况下有效地重建新集合(或简单地分配新集合)?

UPDATE

这个链接中的答案似乎与我想要的有关,但它是一种好的有效方法吗? 有没有更好的方法?

更新2

我得到我的post(编辑覆盖其值),如下所示:

 public async Task GetPostAsync(Guid postId) { return await dbContext.Posts .Include(p => p.Writer) .ThenInclude(u => u.Profile) .Include(p => p.Comments) .Include(p => p.PostCategories) .ThenInclude(pc => pc.Category) .Include(p => p.PostPackages) .ThenInclude(pp => pp.Package) //.AsNoTracking() .SingleOrDefaultAsync(p => p.Id == postId); } 

更新3(我的控制器中的代码,它试图更新post):

 var writerId = User.GetUserId(); var categories = await postService.GetOrCreateCategoriesAsync( vm.CategoryViewModels.Select(cvm => cvm.Name), writerId); var post = await postService.GetPostAsync(vm.PostId); post.Title = vm.PostTitle; post.Content = vm.ContentText; post.PostCategories = categories?.Select(c => new PostCategory { CategoryId = c.Id, PostId = post.Id }).ToArray(); await postService.UpdatePostAsync(post); // Check the implementation in Update4. 

更新4:

 public async Task UpdatePostAsync(Post post) { // Find (load from the database) the existing post var existingPost = await dbContext.Posts .SingleOrDefaultAsync(p => p.Id == post.Id); // Apply primitive property modifications dbContext.Entry(existingPost).CurrentValues.SetValues(post); // Apply many-to-many link modifications dbContext.Set().UpdateLinks( pc => pc.PostId, post.Id, pc => pc.CategoryId, post.PostCategories.Select(pc => pc.CategoryId) ); // Apply all changes to the db await dbContext.SaveChangesAsync(); return existingPost; } 

使用断开链接实体时的主要挑战是检测并应用添加和删除的链接。 而EF Core(截至撰写本文时)几乎没有提供任何帮助。

链接的答案是可以的(自定义的Except方法对于它的IMO来说太重了),但它有一些陷阱 – 必须使用eager / explicit加载提前检索现有的链接(尽管EF Core 2.1懒惰)加载可能不是问题),并且新链接应该只填充FK属性 – 如果它们包含引用导航属性,EF Core将在调用Add / AddRange时尝试创建新的链接实体。

前段时间我回答了类似但略有不同的问题 – 更新EFCore连接的通用方法 。 以下是答案中更通用和优化的自定义通用扩展方法版本:

 public static class EFCoreExtensions { public static void UpdateLinks(this DbSet dbSet, Expression> fromIdProperty, TFromId fromId, Expression> toIdProperty, IEnumerable toIds) where TLink : class, new() { // link => link.FromId == fromId Expression> fromIdVar = () => fromId; var filter = Expression.Lambda>( Expression.Equal(fromIdProperty.Body, fromIdVar.Body), fromIdProperty.Parameters); var existingLinks = dbSet.AsTracking().Where(filter); var toIdSet = new HashSet(toIds); if (toIdSet.Count == 0) { //The new set is empty - delete all existing links dbSet.RemoveRange(existingLinks); return; } // Delete the existing links which do not exist in the new set var toIdSelector = toIdProperty.Compile(); foreach (var existingLink in existingLinks) { if (!toIdSet.Remove(toIdSelector(existingLink))) dbSet.Remove(existingLink); } // Create new links for the remaining items in the new set if (toIdSet.Count == 0) return; // toId => new TLink { FromId = fromId, ToId = toId } var toIdParam = Expression.Parameter(typeof(TToId), "toId"); var createLink = Expression.Lambda>( Expression.MemberInit( Expression.New(typeof(TLink)), Expression.Bind(((MemberExpression)fromIdProperty.Body).Member, fromIdVar.Body), Expression.Bind(((MemberExpression)toIdProperty.Body).Member, toIdParam)), toIdParam); dbSet.AddRange(toIdSet.Select(createLink.Compile())); } } 

它使用单个数据库查询从数据库中检索退出的链接。 开销是很少动态构建的表达式和编译的委托(为了尽可能保持调用代码最简单)和一个临时HashSet用于快速查找。 表达式/委托构建的性能影响应该可以忽略不计,并且可以根据需要进行缓存。

这个想法是仅传递一个链接实体的单个现有密钥和另一个链接实体的退出密钥列表。 因此,根据您要更新的链接实体链接,将以不同方式调用它。

在您的示例中,假设您正在接收IEnumerable postCategories ,过程将是这样的:

 var post = await dbContext.Posts .SingleOrDefaultAsync(someId); dbContext.Set().UpdateLinks(pc => pc.PostId, post.Id, pc => pc.CategoryId, postCategories.Select(pc => pc.CategoryId)); await dbContext.SaveChangesAsync(); 

请注意,此方法允许您更改要求并接受IEnumerable postCategoryIds

 dbContext.Set().UpdateLinks(pc => pc.PostId, post.Id, pc => pc.CategoryId, postCategoryIds); 

IEnumerable postCategories

 dbContext.Set().UpdateLinks(pc => pc.PostId, post.Id, pc => pc.CategoryId, postCategories.Select(c => c.Id)); 

或类似的DTO / ViewModels。

可以以类似的方式更新类别post,并交换相应的选择器。

更新:如果您收到(可能)修改Post post实体实例,整个更新过程冷如下:

 // Find (load from the database) the existing post var existingPost = await dbContext.Posts .SingleOrDefaultAsync(p => p.Id == post.Id); if (existingPost == null) { // Handle the invalid call return; } // Apply primitive property modifications dbContext.Entry(existingPost).CurrentValues.SetValues(post); // Apply many-to-many link modifications dbContext.Set().UpdateLinks(pc => pc.PostId, post.Id, pc => pc.CategoryId, post.PostCategories.Select(pc => pc.CategoryId)); // Apply all changes to the db await dbContext.SaveChangesAsync(); 

请注意,EF Core使用单独的数据库查询来进行预先加载相关的收集。 由于辅助方法的作用相同,因此从数据库中检索主实体时不需要Include链接相关数据。