Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Simplified way to handle one-to-many relationship updates in Entity Framework?

I'd been doing some research looking for a way to simplify the code to update a one-to-many relationship in Entity Framework, there are plenty of similar questions regarding how to update a one-to-many relationships but I couldn't find any simple solution.

Assuming I have the following model classes:

public class Author
{
    public int AuthorId { get; set; }
    ...
    public virtual ICollection<Book> Books { get; set; }
}

public class Book
{
    public int BookId { get; set; }
    ...
    public int? AuthorId { get; set; }
    public virtual Author Author { get; set; }
}


protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Book>()
           .HasOptional(b => b.Author)
           .WithMany(v => v.Books)
           .HasForeignKey(s => s.AuthorId)
           .WillCascadeOnDelete(true);

...

And considering a disconnected scenario where I want to:

  • Add new Books to the collection
  • Remove Books from the collection
  • Modify exisitng Books of the collection

My first approach was to clear the author's book collection and set again with a new one:

var author = authorRepository.GetAuthor(authorId);
author.Books.Clear();
author.Books = updatedBooksCollection;
authorRepository.UpdateAuthor(author);

So, this approach works but it creates orphan records in the database.

One way to solve this is with an Identifying Relationship:

public class Book
{
    [Key, Column(Order = 1), DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int BookId { get; set; }
    ...
    [Key, Column(Order = 2), ForeignKey("Author")]
    public int AuthorId { get; set; }
    public virtual Author Author { get; set; }
}

This works fine when I clear the collection and set a new one BUT I have to change my table schema adding a composite key.

After researching a while I found this article (http://www.entityframeworktutorial.net/EntityFramework4.3/update-one-to-many-entity-using-dbcontext.aspx) which explains clearly how to manage one-to-many relationship updates in a disconnected scenario like this.

I added this method to my repository class:

private void UpdateBooks(IEnumerable<Book> existingBooks, IEnumerable<Book> updatedBooks)
{
    var addedBooks = updatedBooks.Except(existingBooks, x => x.BookId);

        var deletedBooks = existingBooks.Except(updatedBooks, x => x.BookId);

        var modifiedBooks = updatedBooks.Except(addedBooks, x => x.BookId);

        addedBooks.ToList<Book>().ForEach(x => UnitOfWork.Context.Entry(x).State = EntityState.Added);

        deletedBooks.ToList<Book>().ForEach(x => UnitOfWork.Context.Entry(x).State = EntityState.Deleted);

        foreach (var book in modifiedBooks)
        {
            var existingBook = UnitOfWork.Context.Set<Book>().Find(book.BookId);

            if (existingBook != null)
            {
                var contextBook = UnitOfWork.Context.Entry(existingBook);
                contextBook.CurrentValues.SetValues(book);
            }
        }

    UnitOfWork.Context.SaveChanges();
  }

And this works fine, but it seems a lot of code just to handle something trivial as an update of a collection, I don't want to do this for each collection I have in my model when I want to update it.

My question is: Is there a simplified way to handle one-to-many relationship updates in Entity Framework without changing the table schema, I mean without adding a Identifying Relationship?

like image 319
Alan Macgowan Avatar asked Oct 17 '25 17:10

Alan Macgowan


2 Answers

I think what you had is fine with a small change.

var author = authorRepository.GetAuthor(authorId);
//author.Books.Clear();
//author.Books.ToList().ForEach(b => bookRepository.RemoveBook(b)); (Option 1)
context.Books.RemoveRange(author.Books); // (Option 2)
author.Books = updatedBooksCollection;
authorRepository.UpdateAuthor(author);

Clearing the collection is causing your orphan records because Clear only removes the author's FK reference from the book. When the reference is cleared the FK is set as null.

like image 189
James Sampica Avatar answered Oct 20 '25 09:10

James Sampica


Ok, although I accepted @Shoe's answer, I found another simple and reusable way to handle this. I decided to use a generic method in my BaseRepository so I can reuse it for all the other collections by just calling:

UpdateRelatedEntityCollection<Book>(existingBooks, updatedBooks);

This is the refactored code:

BaseRepository class:

public void UpdateRelatedEntityCollection<T>(IEnumerable<T> existingEntities, IEnumerable<T> updatedEntities) where T : BaseRelatedEntity
    {
        var addedEntities = updatedEntities.Except(existingEntities, x => x.Id);

        var deletedEntities = existingEntities.Except(updatedEntities, x => x.Id);

        var modifiedEntities = updatedEntities.Except(addedEntities, x => x.Id);

        addedEntities.ToList<T>().ForEach(x => UnitOfWork.Context.Entry(x).State = EntityState.Added);

        deletedEntities.ToList<T>().ForEach(x => UnitOfWork.Context.Entry(x).State = EntityState.Deleted);

        foreach (var entity in modifiedEntities)
        {
            var existingEntity = UnitOfWork.Context.Set<T>().Find(entity.Id);

            if (existingEntity != null)
            {
                var contextEntry = UnitOfWork.Context.Entry(existingEntity);
                contextEntry.CurrentValues.SetValues(entity);
            }
        }
    }

AuthorRepository class:

   public void UpdateBooks(int id, List<Book> updatedBooks)
    {
        var author = GetAuthorById(id);
        var existingBooks = author.Books.ToList();

        UpdateRelatedEntityCollection<Book>(existingBooks, updatedBooks);

        UnitOfWork.Context.SaveChanges();
    }

Not sure if this is the best way to manage one-to-many relationship updates in Entity Framework but I find it useful for not duplicating code.

like image 21
Alan Macgowan Avatar answered Oct 20 '25 08:10

Alan Macgowan