I am using DDD. I have a class Product which is an aggregate 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 which holds the models doesn't know about EF at all. The problem is that when i call DeleteComment(comment)
, EF throws exception
A relationship from the 'Product_Comments' AssociationSet is in the 'Deleted' state. Given multiplicity constraints, a corresponding 'Product_Comments_Target' must also in the 'Deleted' state.
Even if the element is removed from the collection, EF doesn't delete it. What should i do to fix this without breaking DDD? (I am thinking of making a repository for Comments as well, but is not right)
Code example:
Because i am trying to use DDD, the Product
is an aggregate root, and it has a repository IProductRepository
. A Comment cannot exists without a product, therefore is a children 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
}
}
RemoveRange() method attaches a collection of entities with Deleted state, which in turn will execute the DELETE command for all entities on SaveChanges() . Adding or removing entities using the AddRange and RemoveRange methods improves the performance.
In Entity Framework, the SaveChanges() method internally creates a transaction and wraps all INSERT, UPDATE and DELETE operations under it. Multiple SaveChanges() calls, create separate transactions, perform CRUD operations and then commit each transaction. The following example demonstrates this.
I've seen 3 approaches to workaround this deficiency in EF:
DbContext
's SaveChanges()
and handle the deletion there (as per Euphoric's answer)I like option 3 the best because it doesn't require modification to your database structure (1) or your domain model (2), but puts the workaround in the component (EF) that had the deficiency in the first place.
So this is an updated solution taken from Euphoric's 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: this assumes that ParentProduct
is the navigation property on Comment
back to its owning Product
.
I've seen a lot of people reporting this issue. It's actually quite simple to fix but makes me think there is not enough documentation on how EF is expected to behave in this situation.
Trick: When setting up the relationship between Parent and Child, you'll HAVE TO create a "composite" key on the child. This way, when you tell the Parent to delete 1 or all of its children, the related records will actually be deleted from the database.
To configure composite key using Fluent API:
modelBuilder.Entity<Child>.HasKey(t => new { t.ParentId, t.ChildId });
Then, to delete the 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!
Here is pair of related solutions:
Delete Dependent Entities When Removed From EF Collection
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With