11

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

    }
}
Jehof
  • 34,674
  • 10
  • 123
  • 155
Catalin
  • 11,503
  • 19
  • 74
  • 147
  • 1
    Seems that you aren't calling EF's SaveChanges – EgorBo Nov 20 '12 at 12:54
  • I guess there is a table called Target. This table has FK reference to Comments table. When you try to delete a row in Comment table, the associate rows in Target need to be deleted first. – Eric Fan Nov 20 '12 at 12:54
  • @Nagg I get this error when i call SubmitChanges() – Catalin Nov 20 '12 at 12:55
  • @ErcFan No, i have no Table table, i think this is a default Relationship name created by Entity Framework. (I am using Code First) – Catalin Nov 20 '12 at 12:56
  • @RaraituL Can you explain more code? – Hamlet Hakobyan Nov 20 '12 at 13:31
  • BTw you're not using DDD (at least the question has no relation to DDD), you're using EF. Please don't model your Domain on top of EF or depending on EF or any other ORM or persistence detail – MikeSW Nov 20 '12 at 19:15
  • @HamletHakobyan: I added another example, i hope it is useful. – Catalin Nov 21 '12 at 07:04
  • @MikeSW Domain Layer doesn't know anything about Entity Framework. Domain Layer just works with Repositories and UnitOfWork. – Catalin Nov 21 '12 at 07:04
  • Can you also show us your mapping? I cannot reproduce your error. – Euphoric Nov 21 '12 at 08:35

5 Answers5

14

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!

Mosh
  • 5,944
  • 4
  • 39
  • 44
6

Here is pair of related solutions:

Delete Dependent Entities When Removed From EF Collection

Euphoric
  • 12,645
  • 1
  • 30
  • 44
5

I've seen 3 approaches to workaround this deficiency in EF:

  1. Configure a composite key (as per Mosh's answer)
  2. Raise a domain event and instruct EF to perform the child deletions in its handler (as per this answer)
  3. Override 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.

Community
  • 1
  • 1
BCA
  • 7,776
  • 3
  • 38
  • 53
1

Deleting the Comment from the Product using your approach only deletes the association between Product and Comment. So that the Comment still exists.

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

The way i do it is that i use the Update method of my repository (knows Entity Framework) to check for deleted associations and to also delete the obsolete entities. You can do this by 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);
      }
    }
 }

Sample:

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();
}
Jehof
  • 34,674
  • 10
  • 123
  • 155
  • No, you don't need to touch Context.ObjectStateManager and I strongly recommend you to NEVER play around with context's state, unless you have real reason for doing so. To solve this issue, you'll need to setup a composite key on the child. This way, when you remove the child from its parent, EF will remove the relationship AND delete the child entity as well. Look at my answer below for code example. – Mosh Jul 09 '14 at 04:18
0

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

Wim
  • 1,967
  • 11
  • 19