Remove item from collection using Entity Framework

I am using DDD. I have a Product class, which is an aggregated root.

public class Product : IAggregateRoot { public virtual ICollection<Comment> Comments { get; set; } public void AddComment(Comment comment) { Comments.Add(comment); } public void DeleteComment(Comment comment) { Comments.Remove(comment); } } 

The layer that contains the models does not know about EF at all. The problem is that when I call DeleteComment(comment) , EF throws an exception

The Product_Comments Product Range relationship is in the Deleted state. Given the limitations of multiplicity, the corresponding "Product_Comments_Target" must also be in the "Deleted" state.

Even if an item is removed from the collection, EF does not delete it. What should I do to fix this without breaking DDD? (I also want to create a repository for comments, but not right)

Code example:

Since I'm trying to use DDD, Product is an aggregated root and has an IProductRepository repository. A comment cannot exist without a product, therefore it is a child of Product Aggregate, and Product is responsible for creating and deleting comments. Comment does not have a repository.

 public class ProductService { public void AddComment(Guid productId, string comment) { Product product = _productsRepository.First(p => p.Id == productId); product.AddComment(new Comment(comment)); } public void RemoveComment(Guid productId, Guid commentId) { Product product = _productsRepository.First(p => p.Id == productId); Comment comment = product.Comments.First(p => p.Id == commentId); product.DeleteComment(comment); // Here i get the error. I am deleting the comment from Product Comments Collection, // but the comment does not have the 'Deleted' state for Entity Framework to delete it // However, i can't change the state of the Comment object to 'Deleted' because // the Domain Layer does not have any references to Entity Framework (and it shouldn't) _uow.Commit(); // UnitOfWork commit method } } 
+7
source share
5 answers

I have seen many people who reported this issue. This is actually pretty simple to fix, but it makes me think that there is not enough documentation about how EF is expected to behave in this situation.

Trick: when setting up the relationship between parent and child, you must create a β€œcomposite” key for the child. Thus, when you tell the parent to delete 1 or all of its children, the corresponding records will actually be deleted from the database.

To configure a composite key using the Fluent API:

 modelBuilder.Entity<Child>.HasKey(t => new { t.ParentId, t.ChildId }); 

Then, to remove related children:

 var parent = _context.Parents.SingleOrDefault(p => p.ParentId == parentId); var childToRemove = parent.Children.First(); // Change the logic parent.Children.Remove(childToRemove); // or, you can delete all children // parent.Children.Clear(); _context.SaveChanges(); 

Done!

+11
source

Here are a couple of related solutions:

Delete dependent objects when removed from EF collection

+6
source

I saw 3 approaches to circumvent this drawback in EF:

  • Configure the composite key (according to the Mosh answer)
  • Raise a domain event and instruct EF to do child deletes in its handler (as per this answer)
  • Override DbContext SaveChanges() and handle deletion there (according to euphoric answer)

I like option 3 because it does not require modification of your database structure (1) or your domain model (2), but it places a workaround in the component (EF), which had the drawback in the first place.

So this is an updated solution taken from the Euphoric answer / blog post:

 public class MyDbContext : DbContext { //... typical DbContext stuff public DbSet<Product> ProductSet { get; set; } public DbSet<Comment> CommentSet { get; set; } //... typical DbContext stuff public override int SaveChanges() { MonitorForAnyOrphanedCommentsAndDeleteThemIfRequired(); return base.SaveChanges(); } public override Task<int> SaveChangesAsync() { MonitorForAnyOrphanedCommentsAndDeleteThemIfRequired(); return base.SaveChangesAsync(); } public override Task<int> SaveChangesAsync(CancellationToken cancellationToken) { MonitorForAnyOrphanedCommentsAndDeleteThemIfRequired(); return base.SaveChangesAsync(cancellationToken); } private void MonitorForAnyOrphanedCommentsAndDeleteThemIfRequired() { var orphans = ChangeTracker.Entries().Where(e => e.Entity is Comment && (e.State == EntityState.Modified || e.State == EntityState.Added) && (e.Entity as Comment).ParentProduct == null); foreach (var item in orphans) CommentSet.Remove(item.Entity as Comment); } } 

Note. ParentProduct is ParentProduct to be a navigation property on Comment back to its own Product .

+2
source

Removing a comment from a product using your approach only removes the connection between the Product and the comment. So the comment still exists.

What you need to do is tell ObjectContext that the comment has also been deleted using the DeleteObject() method.

As I do this, I use the Update method of my repository (knows Entity Framework) to check for deleted associations and delete obsolete objects. You can do this using the ObjectStateManager of the ObjectContext.

 public void UpdateProduct(Product product) { var modifiedStateEntries = Context.ObjectStateManager.GetObjectStateEntries(EntityState.Modified); foreach (var entry in modifiedStateEntries) { var comment = entry.Entity as Comment; if (comment != null && comment.Product == null) { Context.DeleteObject(comment); } } } 

Example:

 public void RemoveComment(Guid productId, Guid commentId) { Product product = _productsRepository.First(p => p.Id == productId); Comment comment = product.Comments.First(p => p.Id == commentId); product.DeleteComment(comment); _productsRepository.Update(product); _uow.Commit(); } 
+1
source

I solved the same problem by creating a parent attribute for my models and checking the attribute in the SaveChanges function. I wrote a blog about this: http://wimpool.nl/blog/DotNet/extending-entity-framework-4-with-parentvalidator

0
source

All Articles