When I am in a detached scenario and get a dto from the client which I map into an entity to save it I do this:
context.Entry(entity).State = EntityState.Modified; context.SaveChanges();
For what is then the DbSet.Attach(entity)
or why should I use the .Attach method when EntityState.Modified already attaches the entity?
Add is to indicate to the Entity Framework that we are going to want to insert a new record in the database. In contrast, Entry and Attach are used in scenarios where the record already exists in the database, and we simply want to make some kind of modification on it. var attach = context.
DbContext generally represents a database connection and a set of tables. DbSet is used to represent a table. Your code sample doesn't fit the expected pattern. First, it is incomplete. Also, there are properties that really don't belong.
EntityState.Added : EntityState.Modified; context.SaveChanges(); } } Note that when you change the state to Modified all the properties of the entity will be marked as modified and all the property values will be sent to the database when SaveChanges is called.
Attach is used to repopulate a context with an entity that is known to already exist in the database. SaveChanges will therefore not attempt to insert an attached entity into the database because it is assumed to already be there.
When you do context.Entry(entity).State = EntityState.Modified;
, you are not only attaching the entity to the DbContext
, you are also marking the whole entity as dirty. This means that when you do context.SaveChanges()
, EF will generate an update statement that will update all the fields of the entity.
This is not always desired.
On the other hand, DbSet.Attach(entity)
attaches the entity to the context without marking it dirty. It is equivalent to doing context.Entry(entity).State = EntityState.Unchanged;
When attaching this way, unless you then proceed to update a property on the entity, the next time you call context.SaveChanges()
, EF will not generate a database update for this entity.
Even if you are planning on making an update to an entity, if the entity has a lot of properties (db columns) but you only want to update a few, you may find it advantageous to do a DbSet.Attach(entity)
, and then only update the few properties that need updating. Doing it this way will generate a more efficient update statement from EF. EF will only update the properties you modified (in contrast to context.Entry(entity).State = EntityState.Modified;
which will cause all properties/columns to be updated)
Relevant documentation: Add/Attach and Entity States.
Code example
Let's say you have the following entity:
public class Person { public int Id { get; set; } // primary key public string FirstName { get; set; } public string LastName { get; set; } }
If your code looks like this:
context.Entry(personEntity).State = EntityState.Modified; context.SaveChanges();
The SQL generated will look something like this:
UPDATE person SET FirstName = 'whatever first name is', LastName = 'whatever last name is' WHERE Id = 123; -- whatever Id is.
Notice how the above update statement will update all the columns, regardless or whether you've actually changed the values or not.
In contrast, if your code uses the "normal" Attach like this:
context.People.Attach(personEntity); // State = Unchanged personEntity.FirstName = "John"; // State = Modified, and only the FirstName property is dirty. context.SaveChanges();
Then the generated update statement is different:
UPDATE person SET FirstName = 'John' WHERE Id = 123; -- whatever Id is.
As you can see, the update statement only updates the values that were actually changed after you attached the entity to the context. Depending on the structure of your table, this can have a positive performance impact.
Now, which option is better for you depends entirely on what you are trying to do.
Just in addition (to the marked answer) there is an important difference between context.Entry(entity).State = EntityState.Unchanged
and context.Attach(entity)
(in EF Core):
I did some tests to understand it more by myself (therefore this also includes some general reference testing), so this is my test-scenario:
QueryTrackingBehavior.NoTracking
These are the models:
public class Order { public int Id { get; set; } public string Comment { get; set; } public string ShippingAddress { get; set; } public DateTime? OrderDate { get; set; } public List<OrderPos> OrderPositions { get; set; } [ForeignKey("OrderedByUserId")] public User OrderedByUser { get; set; } public int? OrderedByUserId { get; set; } } public class OrderPos { public int Id { get; set; } public string ArticleNo { get; set; } public int Quantity { get; set; } [ForeignKey("OrderId")] public Order Order { get; set; } public int? OrderId { get; set; } } public class User { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } }
This is the (original) test data in the database:
To get the order:
order = db.Orders.Include(o => o.OrderPositions).Include(o => o.OrderedByUser).FirstOrDefault();
Now the tests:
Simple Update with EntityState:
db.Entry(order).State = EntityState.Unchanged; order.ShippingAddress = "Germany"; // will be UPDATED order.OrderedByUser.FirstName = "William (CHANGED)"; // will be IGNORED order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be IGNORED order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED db.SaveChanges(); // Will generate SQL in 2 Calls: // INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555', 1, 5) // UPDATE [Orders] SET [ShippingAddress] = 'Germany' WHERE [Id] = 1
Simple Update with Attach:
db.Attach(order); order.ShippingAddress = "Germany"; // will be UPDATED order.OrderedByUser.FirstName = "William (CHANGED)"; // will be UPDATED order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be UPDATED order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED db.SaveChanges(); // Will generate SQL in 1 Call: // UPDATE [OrderPositions] SET [ArticleNo] = 'K-1234' WHERE [Id] = 1 // INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555 (NEW)', 1, 5) // UPDATE [Orders] SET [ShippingAddress] = 'Germany' WHERE [Id] = 1 // UPDATE [Users] SET [FirstName] = 'William (CHANGED)' WHERE [Id] = 1
Update with changing Child-Ids with EntityState:
db.Entry(order).State = EntityState.Unchanged; order.ShippingAddress = "Germany"; // will be UPDATED order.OrderedByUser.Id = 3; // will be IGNORED order.OrderedByUser.FirstName = "William (CHANGED)"; // will be IGNORED order.OrderPositions[0].Id = 3; // will be IGNORED order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be IGNORED order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED db.SaveChanges(); // Will generate SQL in 2 Calls: // INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555', 1, 5) // UPDATE [Orders] SET [ShippingAddress] = 'Germany' WHERE [Id] = 1
Update with changing Child-Ids with Attach:
db.Attach(order); order.ShippingAddress = "Germany"; // would be UPDATED order.OrderedByUser.Id = 3; // will throw EXCEPTION order.OrderedByUser.FirstName = "William (CHANGED)"; // would be UPDATED order.OrderPositions[0].Id = 3; // will throw EXCEPTION order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // would be UPDATED order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // would be INSERTED db.SaveChanges(); // Throws Exception: The property 'Id' on entity type 'User' is part of a key and so cannot be modified or marked as modified. To change the principal of an existing entity with an identifying foreign key first delete the dependent and invoke 'SaveChanges' then associate the dependent with the new principal.)
Note: This throws Exception, no matter if the Id was changed or was set to the original value, seems like the state of Id is set to "changed" and this is not allowed (because it's the primary key)
Update with changing Child-Ids as new (no difference between EntityState and Attach):
db.Attach(order); // or db.Entry(order).State = EntityState.Unchanged; order.OrderedByUser = new User(); order.OrderedByUser.Id = 3; // // Reference will be UPDATED order.OrderedByUser.FirstName = "William (CHANGED)"; // will be UPDATED (on User 3) db.SaveChanges(); // Will generate SQL in 2 Calls: // UPDATE [Orders] SET [OrderedByUserId] = 3, [ShippingAddress] = 'Germany' WHERE [Id] = 1 // UPDATE [Users] SET [FirstName] = 'William (CHANGED)' WHERE [Id] = 3
Note: See the difference to the Update with EntityState without new (above). This time the Name will be updated, because of the new User instance.
Update with changing the Reference-Ids with EntityState:
db.Entry(order).State = EntityState.Unchanged; order.ShippingAddress = "Germany"; // will be UPDATED order.OrderedByUserId = 3; // will be UPDATED order.OrderedByUser.Id = 2; // will be IGNORED order.OrderedByUser.FirstName = "William (CHANGED)"; // will be IGNORED order.OrderPositions[0].Id = 3; // will be IGNORED order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be IGNORED order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED db.SaveChanges(); // Will generate SQL in 2 Calls: // INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555', 1, 5) // UPDATE [Orders] SET [OrderedByUserId] = 3, [ShippingAddress] = 'Germany' WHERE [Id] = 1
Update with changing the Reference-Ids with Attach:
db.Attach(order); order.ShippingAddress = "Germany"; // will be UPDATED order.OrderedByUserId = 3; // will be UPDATED order.OrderedByUser.FirstName = "William (CHANGED)"; // will be UPDATED (on FIRST User!) order.OrderPositions[0].ArticleNo = "K-1234 (CHANGED)"; // will be UPDATED order.OrderPositions.Add(new OrderPos { ArticleNo = "T-5555 (NEW)", Quantity = 5 }); // will be INSERTED db.SaveChanges(); // Will generate SQL in 1 Call: // UPDATE [OrderPositions] SET [ArticleNo] = 'K-1234' WHERE [Id] = 1 // INSERT INTO [OrderPositions] ([ArticleNo], [OrderId], [Quantity]) VALUES ('T-5555 (NEW)', 1, 5) // UPDATE [Orders] SET [OrderedByUserId] = 3, [ShippingAddress] = 'Germany' WHERE [Id] = 1 // UPDATE [Users] SET [FirstName] = 'William (CHANGED)' WHERE [Id] = 1
Note: The reference will be changed to User 3, but also the user 1 will be updated, I guess this is because the order.OrderedByUser.Id
is unchanged (it's still 1).
Conclusion With EntityState you have more control, but you have to update sub-properties (second-level) by yourself. With Attach you can update everything (I guess with all levels of properties), but you have to keep an eye on references. Just for example: If User (OrderedByUser) would be a dropDown, changing the value via a dropDown might be overwrite the whole User-object. In this case the original dropDown-Value would be overwritten instead of the reference.
For me the best case is setting objects like OrderedByUser to null and only set the order.OrderedByUserId to the new value, if I only want change the reference (no matter if EntityState or Attach).
Hope this helps, I know it's a lot of text :D
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