how DbContext.AttachRange() works in this scenario

asp.net-core-mvc c# entity-framework-6 entity-framework-core

Question

I saw a book with some code like this:

public class Order 
{
   public int OrderID { get; set; }
   public ICollection<CartLine> Lines { get; set; }
   ...
}

public class CartLine
{
   public int CartLineID { get; set; }
   public Product Product { get; set; }
   public int Quantity { get; set; }
}

//Product class is just a normal class that has properties such as ProductID, Name etc

and in the order repository, there is a SaveOrder method:

public void SaveOrder(Order order)
{
   context.AttachRange(order.Lines.Select(l => l.Product));
   if (order.OrderID == 0)
   {
       context.Orders.Add(order);
   }
   context.SaveChanges();
}

and the book says:

when store an Order object in the database. When the user’s cart data is deserialized from the session store, the JSON package creates new objects that are not known to Entity Framework Core, which then tries to write all the objects into the database. For the Product objects, this means that Entity Framework Core tries to write objects that have already been stored, which causes an error. To avoid this problem, I notify Entity Framework Core that the objects exist and shouldn’t be stored in the database unless they are modified

I'm confused, and have two questions:

Q1-why writing objects that have already been stored will cause an error, in the point of view of underlying database, it's just an update SQL statement that modify all columns to their current values?I know it does unnecessary works by changing nothing and rewrite everything, but it shouldn't throw any error in database level?

Q2-why we don't do the same thing to CartLine as:

context.AttachRange(order.Lines.Select(l => l.Product));
context.AttachRange(order.Lines);

to prevent CartLine objects stored in the database just as the way we do it to Product object?

1
0
7/22/2019 5:42:33 AM

Accepted Answer

Okay, so this is gonna be a long one:

1st Question:

In Entity Framework (core or "old" 6), there's this concept of "Change tracking". The DbContext class is capable of tracking all the changes you made to your data, and then applying it in the DB via SQL statements (INSERT, UPDATE, DELETE). To understand why it throws an error in your case, you first need to understand how the DbContext / change tracking actually works. Let's take your example:

public void SaveOrder(Order order)
{
   context.AttachRange(order.Lines.Select(l => l.Product));
   if (order.OrderID == 0)
   {
       context.Orders.Add(order);
   }
   context.SaveChanges();
}

In this method, you receive an Order instance which contains Lines and Products. Let's assume that this method was called from some web application, meaning you didn't load the Order entity from the DB. This is what's know as the Disconected Scenario

It's "disconnected" in the sense that your DbContext is not aware of their existence. When you do context.AttachRange you are literally telling EF: I'm in control here, and I'm 100% sure these entities already exist in the DB. Please be aware of them for now on!,

Let's use your code again: Imagine that it's a new Order (so it will enter your if there) and you remove the context.AttachRange part of the code. As soon as the code reaches the Add and SaveChanges these things will happen internally in the DbContext:

  1. The DetectChanges method will be called
  2. It will try to find all the entities Order, Lines and Products in its current graph
  3. If it doesn't find them, they will be added to the "pending changes" as a new records to be inserted

Then you continue and call SaveChanges and it will fail as the book tells you. Why? Imagine that the Products selected were:

Id: 1, "Macbook Pro"
Id: 2, "Office Chair"

When the DbContext looked at the entities and didn't know about them, it added them to the pending changes with a state of Added. When you call SaveChanges, it issues the INSERT statements for these products based on their current state in the model. Since Id's 1 and 2 already exists in the database, the operation failed, with a Primary Key violation.

That's why you have to call Attach (or AttachRange) in this case. This effectively tells EF that the entities exist in the DB, and it should not try to insert them again. They will be added to the context with a state of Unchanged. Attach is often used in these cases where you didn't load the entities from the dbContext before.

2nd question:

This is hard for me to access because I don't know the context/model at that level, but here's my guess:

You don't need to do that with the Cartline because with every order, you probably want to insert new Order line. Think like buying stuff at Amazon. You put the products in the cart and it will generate an Order, then Order Lines, things that compose that order.

If you were then to update an existing order and add more items to it, then you would run into the same issue. You would have to load the existing CartLines prior to saving them in the db, or call Attach as you did here.

Hope it's a little bit clearer. I have answered a similar question where I gave more details, so maybe reading that also helps more: How does EF Core Modified Entity State behave?

1
7/22/2019 11:35:46 AM


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow