How does EF Core Modified Entity State behave?

2020-07-10 09:18发布

问题:

Does it matter we put the entity state = modified after changes or before making changes?

using (var db = new LakshyaContext())
{
    foreach (var category in db.Categories)
    {
        db.Entry(category).State = EntityState.Modified; // before
        category.Count = 25; //Making Changes
        db.Entry(category).State = EntityState.Modified; //After
    }

    db.SaveChanges();
}

回答1:

So first, let's get the most important thing out of the way:

You are right. In your example, you don't need to manually call db.Entry(category).State = EntityState.Modified. This is because you are loading the entries (categories) from the context above. This is known as the "Connected Scenario" where the DbContext is aware of the entities, it's tracking them. This is the same, for instance in an ASP.NET Core app, where the context is shared across the HTTP request.

Any modification you make between the scope of using (var db = new LakshyaContext()), will be known by the context when you call SaveChanges.

Now, when working on disconnected scenarios (as you said UnTracked entities), we have to dig a little bit deeper.

To understand that, first you need to know how the DbContext know what's changed. Take the following example:

using (var context = new MyContext())
{
    // loads the book by it's ISBN
    var book = context.Books
        .Single(p => p.ISBN == "123456");

    // Do changes
    book.Price = 30;

    // Save changes
    context.SaveChanges();
}

How does it know that the Price changed? since it's just a normal auto property on the Book class? The magic lies behind the DetectChanges method.

In some specific cases, the DbContext calls the DetectChanges method. The most obvious one is when SaveChanges is called. In a top level, the way it works is:

  1. The DbContext makes a snapshot of each entity it loads
  2. When SaveChanges is called, it will proceed to call DetectChanges which will do it's magic to figure it out what's changed or not.
  3. DbContext then takes care of sending the correct commands to the db.

At this point, we know the responsibility of DetectChanges. The important part now is knowing when DetectChanges is called (apart from SaveChanges that we already know). This is crucial to finally answer your "Order" question. From the linked article from Arthur Vickers

The methods that call DetectChanges:

  • DbSet.Find
  • DbSet.Local
  • DbSet.Remove
  • DbSet.Add
  • DbSet.Attach
  • DbContext.SaveChanges
  • DbContext.GetValidationErrors
  • DbContext.Entry
  • DbChangeTracker.Entries

Let's examine this code that demonstrates the "disconnected" scenario.

public Task UpdateBook() 
{
    Book book = null;

    // Just loads the book from this context
    using (var context = new MyContext())
    {
        book = context.Books
            .Single(p => p.ISBN == "123456");       
    }

    // Starts a new context where the book is going to be updated
    using (var anotherContext = new MyContext())
    {
        // Changed the price - remember this is not loaded from this context!
        book.Price = 40;

        // THIS IS KEY: This will call `DetectChanges`      
        // This entity will be tracked by the context now
        db.Entry(book).State = EntityState.Modified

        // Update will occur normally
        db.SaveChanges();
    }
}

When we go into the second DbContext, it is not aware of our book entity. We change the price and then call db.Entry(book).State = EntityState.Modified. At this point, the DbContext will start tracking it, and DetectChanges is invoked. Proceeding calling SaveChanges will work as expected.

If we had done the opposite, calling db.Entry(book).State = EntityState.Modified before actually changing the price things would.... still work!

Why? Well, manually changing the state of the entity with db.Entry(book).State will add the entity to the context, meaning it will start tracking it for changes. So, even if we call db.Entry(book).State and then apply changes on the entity it will not matter because calling SaveChanges at the end, will trigger again DetectChanges, and since it was already called before, there was already a snapshot in place for the entity.

One way you can verify this behavior yourself is running the code above with logging enabled for the DbContext:

// Calling db.Entry.. produces this log:

DetectChanges starting for 'MyContext'.
Microsoft.EntityFrameworkCore.ChangeTracking:Debug: DetectChanges completed for 'MyContext'.
Context 'MyContext' started tracking 'Book' entity.


// Calling SaveChanges produces this log:

SaveChanges starting for 'MyContext'
DetectChanges starting for 'MyContext'.
DetectChanges completed for 'MyContext'.
Opening connection to database 'BooksDB'
Beginning transaction with isolation
...

Now some remarks:

The update above in the disconnected scenario will issue an update on ALL COLUMNS in the table. This might not be what you expected. There are ways to prevent this. Read more here

DetectChanges does a lot of stuff internally, not only applying merges on changes. It takes care of Foreign Keys, updating references of navigation properties and more, and doing "fixup".

More resources to read on: (especially the ones from Arthur Vickers!)

Secrets of DetectChanges Part 1: What does DetectChanges do?

Secrets of DetectChanges Part 2: When is DetectChanges called automatically?

Possible Issue with Change Tracker Caching Entity State EF Core 2.0.2

Working with Disconnected Entity Graph in Entity Framework Core

Entity Framework Core TrackGraph For Disconnected Data