13

I am struggling to understand something with change tracking in EF6.

I have code similar to this.

public class SomeClass
{
    private List<User> _users;
    private DAL _dal;

    public void ProcessUsers()
    {
        _users = _dal.GetUsers();

        foreach(var u in users)
        {
            u.user.Comment = "This is a test";
        }

        _dal.SaveChanges();
    }
}

The DAL class looks a little like this.

public class DAL
{
    ...
    private DataContext _context; // Assume that this is being newed up in a constructor.

    public List GetUsers()
    {
        return _context.Users.ToList();
    }

    public void SaveChanges()
    {
        _context.SaveChanges();
    }
}

So as we can see from the code in the ProcessUsers method we have a list of users and we are modifying that list.

Now I know that this works, Its the way I have always done it however I was always under the impression that the objects in the List (Users in this case) were a reference back to the corresponding object in the DBSet Local collection.

After a bit of thought I am not sure that this is the case as if the context is disposed the list is still populated and can be manipulated (We just loose the ability to push it back to the database without some additional work) so from that perspective the items in the list must be copies of the items from the DBSet Local collection... but if that is the case I wouldn't have though that manipulating an object in the list would have any effect on the object in the dbset as it would be a copy.

In Summary

The question is what happens when I call ToList on a DBSet and how does change tracking work in this instance? - I know it does work, but I think my current understanding might be incorrect.

Flores
  • 8,226
  • 5
  • 49
  • 81
D3vy
  • 831
  • 7
  • 19

3 Answers3

1

EF has a collection where all the pending changes are tracked (_context.ObjectStateManager, see here...). Further more loading entities with EF you get a proxy instance instead of your real entity-class. Using this proxy EF is "injecting" code into your entity instances which updates change tracking information.

When you dispose your context you loose this information. To add the existing entity instance to another context you can use the _context.Attach() method.

The SaveChanges() does process the _context.ObjectStateManager information.

Marc
  • 4,715
  • 3
  • 27
  • 34
  • 2
    Hi, Sorry I dont think I have been clear in my question, I know how change tracking works, and the code in the example above works. What I suppose I am asking is when I call toList on a DBset what goes into that list... copies of the items in the dbSet or references to those objects? After a bit of digging and speaking to others at work Im fairly sure that the list MUST be populated with references to the contents of the dbSet rather than copies. – D3vy Jan 11 '17 at 17:44
  • Correct, @D3vy the list will contain references to the same objects that are loaded into the `DBSet` - unless _Change Tracking_ is disabled, in which case calling `.ToList()` will operate on the `IQueryable` and the objects will be enumerated directly from the underlying call to the DB and will not be stored at all. – Chris Schaller Dec 28 '22 at 22:48
1

If you ignore EF for a moment, IEnumerable<T>.ToList() enumerates the source and stores a reference to each of the values in a List.

If the source was an IQueryable<T> then calling .ToList() will force the evaluation of the query and will enumerate the results as above, storing a reference to each result in a List.

  • .ToArray has similar behavior but returns an array instead of a List<T>.

Now, with EF6 DBSet<T> by default, when the underlying query is evaluated, the results will be tracked by ObjectStateManager within the context. You can also access the most recent results via the DBSet<T>.Local collection.

The question is what happens when I call ToList on a DBSet and how does change tracking work in this instance?

A direct answer is that .ToList() does not affect change tracking at all, as part of the enumeration, before each record is yielded to the caller a reference to it is stored in the ObjectStateManager of the Context IF change tracking has not been disabled.

Your understanding in this regard is correct:

I was always under the impression that the objects in the List (Users in this case) were a reference back to the corresponding object in the DBSet Local collection.

What is not correct is your understanding of Dipose(). Calling Dispose() (or setting a reference to null) does not directly release the memory associated with the underlying object. All it does is reduce the reference count to the underlying object. The Garbage Collector will later evaluate the references to each of the objects held in memory to determine if they have any references that are still currently in scope. Only when the object does not have any current valid references will it actually be deallocated. This is often why we use .ToList() in c# to cache objects into a locally scoped variable to prevent them from being deallocated.

  • With regard to Reference Types, .Tolist() does not clone the objects into a new List, it only adds a new reference to the underlying object and stores that in the List. Only Value Types will be stored as a copy when added to a List or Array in C#.

When the Context is disposed the data cached into any Lists or Arrays outside of that context will not be destroyed. However any queries will no longer be accessible and neither will the Change Tracking because the ObjectStateManager within the Context will also be disposed. So once you have disposed of the Context any previously tracked objects are now considered to be in a Detached state.

If the objects from the Context retain any references outside of the now disposed Context, then those references will still be valid and the object will still exist.

Change Tracking can still sort-of be applied for these objects against a new instance of the Context if you need it, you first need to Attach() the objects into the new Context for them to be tracked again. There are other helper methods to achieve this, but for updates it is sometimes important to be aware that at the point in time that you Attach the object to the context only after that are new changes for individual properties actually tracked. If you deliberately set the state for the whole object to Modified then it is assumed that all properties in that object have been changed to their current state.

  • This can cause issues for some patterns of business logic, it is out of scope to go into that detail here, but it is a behavior that differentiates how Loaded and Attached objects in the EF6 ObjectStateManager are actually tracked.
Chris Schaller
  • 13,704
  • 3
  • 43
  • 81
0

You need to use context.TableName.Update(obejct)to mark updated object. Next save changes using context.Savechanges(); So in your example

public void ProcessUsers()
{
    _users = _dal.GetUsers();

    foreach(var u in users)
    {
        u.user.Comment = "This is a test";
        _dal.Users.Update(u);
    }

    _dal.SaveChanges();
}
MKasprzyk
  • 503
  • 5
  • 17
  • Well, no, thats my point, the code that I posted isnt broken, it works as is. I just want to understand what the ToList() on the DBSet does, I assume now that the list is populated with references to the original objects from the dbset Local collection. But from what I have read its not, they are copies - however if this is the case the change tracking would not work and the above code would fail. – D3vy Jan 11 '17 at 12:52