DbSet.Attach(entity) vs DbContext.Entry(entity).State = EntityState.Modified

109,303

Solution 1

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.

Solution 2

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:

  • I used EF Core 3.1.3
  • I used QueryTrackingBehavior.NoTracking
  • I used only attributes for mapping (see below)
  • I used different contexts to get the order and to update the order
  • I wiped the whole db for every test

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: enter image description here

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

Solution 3

When you use the DbSet.Update method, Entity Framework marks all properties of your entity as EntityState.Modified, so tracks them. If you want to change only some of your properties, not all of them, use DbSet.Attach. This method makes all of your properties EntityState.Unchanged, so you must make your properties that you want to update EntityState.Modified. Thus when the app hits to DbContext.SaveChanges, it will only operate modified properties.

Share:
109,303
Elisabeth
Author by

Elisabeth

I am a develoBee summ summ :P

Updated on July 08, 2022

Comments

  • Elisabeth
    Elisabeth almost 2 years

    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?

  • bubi
    bubi almost 9 years
    EF does not generate the WHERE clause in this way. If you attached an entity created with new (i.e. new Entity()) and set it to modified you have to set all the original fields because of optimistic lock. The WHERE clause generated in UPDATE query usually contains all the original fields (not only Id) so if you don't do so EF will throw a concurrency exception.
  • sstan
    sstan almost 9 years
    @budi: Thank you for your feedback. I re-tested to be sure, and for a basic entity, it does behave as I described, with the WHERE clause containing only the primary key, and without any concurrency check. To have concurrency checking, I need to explicitly configure a column as a concurrency token or rowVersion. In that case, the WHERE clause will only have the primary key and the concurrency token column, not all the fields. If your tests show otherwise, I would love to hear about it.
  • Navid_pdp11
    Navid_pdp11 almost 8 years
    how can i dynamically find witch property is modified?
  • Shimmy Weitzhandler
    Shimmy Weitzhandler almost 7 years
    @Navid_pdp11 DbContext.Entry(person).CurrentValues and DbContext.Entry(person).OriginalValues.
  • jayasurya_j
    jayasurya_j over 4 years
    might slightly be off topic, but if I use a repository pattern, i have to create a repository for every model as every model has some entity that needs to be in untracked state while inserting a new record in db, so I can't have a generic repository that attaches entities to context during insert. How do you handle this best?
  • dstj
    dstj about 4 years
    I found that one difference between context.Entry(entity).State = EntityState.Unchanged and context.Attach(entity) is that Attach() will also track all linked object's properties (e.g. entity.OwnedEntity.Xyz), whereas Entry(entity).State will only track the entity's "first level" properties.