Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Adding items to a collection using Entity Framework

I'm trying to follow the DDD Repository pattern with Entity Framework 4. But I'm having problems saving changes to collection properties of my aggregate roots. Consider my classes below. Item is my aggregate root which contains a collection of SubItem entities.

public class Item
{
    public int ItemId { get; set; }
    public string Name { get; set; }
    public ICollection<SubItem> SubItems { get; private set; }

    public Item()
    {
        this.SubItems = new HashSet<SubItem>();
    }
}

public class SubItem
{
    public int ItemId { get; set; }
    public int SubItemId { get; set; }
    public string Name { get; set; }
}

Next I defined a repository interface for my aggregate root class

public interface IItemRespository
{
    Item Get(int id);
    void Add(Item i);
    void Save(Item i);
}

Now here is my DbContext class that sets up the EF mapping.

public class ItemContext : System.Data.Entity.DbContext
{
    protected override void OnModelCreating(System.Data.Entity.DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Item>().HasKey(i => i.ItemId);
        modelBuilder.Entity<Item>().Property(i => i.Name);

        modelBuilder.Entity<Item>().HasMany(i => i.SubItems)
            .WithRequired()
            .HasForeignKey(si => si.ItemId);

        modelBuilder.Entity<SubItem>().HasKey(i => i.SubItemId);
        modelBuilder.Entity<SubItem>().Property(i => i.Name);
    }
}

Finally here's my implementation of IRepository using the DBContext

public class Repository : IItemRespository
{
    public void Save(Item i)
    {
        using (var context = new ItemContext())
        {
            context.Set<Item>().Attach(i);
            context.SaveChanges();
        }
    }

    public Item Get(int id)
    {
        using (var context = new ItemContext())
        {
            var result = (from x in context.Set<Item>() where x.ItemId == id select x).FirstOrDefault();
            return result;
        }
    }

    public void Add(Item i)
    {
        using (var context = new ItemContext())
        {
            context.Set<Item>().Add(i);
            context.SaveChanges();
        }
    }
}

The following code creates a new Item, adds it to the repository, adds some new SubItems, and then saves the changes.

IItemRespository repo = new Repository();

//Create a new Item
Item parent = new Item() { Name = "Parent" };
repo.Add(parent);

//A long period of time may pass .. . .

//later add sub items
parent.SubItems.Add(new SubItem() { Name = "Child 1" });
parent.SubItems.Add(new SubItem() { Name = "Child 2" });   
parent.SubItems.Add(new SubItem() { Name = "Child 3" });

//save the added sub items
repo.Save(parent);

I get the following exception when the Save() method tries to attach the item to the context.

A referential integrity constraint violation occurred: The property values that define the referential constraints are not consistent between principal and dependent objects in the relationship.

I realized I'm creating a new context for each method in the repository. This is intentional. A long period of time may pass between when an Item is added and then later edited, and I don't want to keep the context or database connection open for the entire time.

Now if I attach the newItem to the second context before adding the sub items as in the code below it works.

//Create a new item
Item newItem = new Item() { Name = "Parent" };

using (ItemContext context1 = new ItemContext())
{
    //Create a new aggrgate
    context1.Set<Item>().Add(newItem);
    context1.SaveChanges();
}

//Long period of time may pass

using (ItemContext context2 = new ItemContext())
{
    context2.Set<Item>().Attach(newItem);

    newItem.Name = "Edited Name";
    newItem.SubItems.Add(new SubItem() { Name = "Child 1" });
    newItem.SubItems.Add(new SubItem() { Name = "Child 2" });
    newItem.SubItems.Add(new SubItem() { Name = "Child 3" });

    context2.SaveChanges();
}

However, if I want to be true to the repository pattern the code that edits Item shouldn't know anything about how the repository works or the ItemContext class. It should simply be able to make changes to an aggregate root entity, and then save those changes through a repository Save() method.

So how do I modify my Save() method so that changes to Item.SubItems are saved correctly?

like image 985
Eric Anastas Avatar asked Nov 08 '12 04:11

Eric Anastas


People also ask

Which method can be used to add a collection of entities in one go?

Entity Framework 6 introduced methods to add and remove a collection of entities in one go. The DbSet. AddRange() method attaches a collection of entities to the context with Added state, which will execute the INSERT command in the database for all entities on SaveChanges() .

How do I add a new record in Entity Framework?

Use the DbSet. Add method to add a new entity to a context (instance of DbContext ), which will insert a new record in the database when you call the SaveChanges() method.

How do I insert multiple rows in Entity Framework?

You can add multiple records or multiple objects using the AddRange method of DbSet as shown in the following code. The code creates a list of department objects and inserts two new departments to the list. We add the list to the context using the AddRange method.

How to add multiple records in Entity Framework Core?

Add Record / Multiple Records In Entity Framework Core. In this tutorial let us look at how to add a record, add multiple records to the database. Before inserting records into the database, we must add the entities to the context first. To do that we use the Add & AddRange methods. Once the records are added to the context, ...

How to add a list to the context in EF?

We add the list to the context using the AddRange method. The EF will save only those records, which you add to the context using the Add method or AddRange method. For Example, consider the following example. We retrieve all the Departments into the deps List.

What does the Entity Framework do?

The EF also takes care of updating identity values generated in the database in the entity. We also show you how to add related entities or data. The source code of this project available in GitHub. The code below creates a new instance of the Department object.

How do I add a department to a list in EF?

The code creates a list of department objects and inserts two new departments to the list. We add the list to the context using the AddRange method. The EF will save only those records, which you add to the context using the Add method or AddRange method.


1 Answers

You'll need to help EF by setting some properties to get this to work.

When you create a new subitem, you'll need to set the FK yourself:

   parent.SubItems.Add(new SubItem() { Name = "Child 1", ItemId = parent.ItemId});
   parent.SubItems.Add(new SubItem() { Name = "Child 2", ItemId = parent.ItemId });
   parent.SubItems.Add(new SubItem() { Name = "Child 3", ItemId = parent.ItemId });

And then in your save function, add or attach the items to your context:

    public void Save(Item i)
    {
        using (var context = new ItemContext())
        {
            foreach (var subitem in i.SubItems)
            {
                if (subitem.SubItemId == 0)
                    context.Set<SubItem>().Add(subitem);
                else
                    context.Set<SubItem>().Attach(subitem);
            }
            context.Set<Item>().Attach(i);
            context.SaveChanges();
        }
    }

The reason is, because your entity isn't attached to a context when you're doing the attach, EF doesn't actually know where the entities came from - it thought that the FK not being set (probably 0) was a valid state - which is where your error was coming from. The reason you need to attach the child objects first, is so that you can actually add rather than attach. Again, since your context wasn't alive when the subitem was attached, EF isn't sure where the entity came from, and assumes the 0 PK is correct, creating your error.

like image 198
Mark Oreta Avatar answered Oct 08 '22 11:10

Mark Oreta