2

Given the following function the variable currentModel is already the modified model that I want to update and it might have some properties different from the ones in the database and this function correctly updates the modified values.

Now I want to track the changes made before the update, the problem is that the ChangeTracker is detecting all properties as modified even when only one is acctualy different from the original model.

Is there a way to use ChangeTracker while also updating the statement with EntityState.Modified (reference)?

Here is the function used:

public void SaveCustomer(Clients currentModel)
        {
            var objFromDbAct = _db.Clients.Local.FirstOrDefault(u => u.Recid == currentModel.Recid);
            if (objFromDbAct != null) { _db.Entry(objFromDbAct).State = EntityState.Detached; }
            _db.Entry(currentModel).State = EntityState.Modified;
           
            _db.ChangeTracker.DetectChanges();
            string trackChanges = _db.ChangeTracker.DebugView.LongView;

            _db.SaveChanges();            
        }

Here is the return from ChangeTracker.DebugView.LongView (I have removed some fields to simplify, but the same applies to all of them. In this case only Zip was changed.

Clients {Recid: 6391} Modified
  Recid: 6391 PK
  Additional: '' Modified
  Addr1: '12345 Somewhere' Modified
  Addr2: '' Modified
  Addr3: <null> Modified
  Zip: '000002222' Modified
  PortalUsers: <null>
  • When all fields are updated, `objFromDbAct` is null? – vernou Oct 19 '22 at 22:45
  • objFromDbAct checks to see if the record is already tracked and detaches if it is. It is not related to the fields that are updated. – Francisco Souza Oct 20 '22 at 12:07
  • All fields are updated in both case? If `objFromDbAct` is null, sound logic all fields are updated. But when `objFromDbAct` isn't null, then `Object.ReferenceEquals(objFromDbAct, currentModel)` will be true. In this case, `DbContext.SaveChanges` will do the job. – vernou Oct 20 '22 at 14:05
  • How do you expect EF to know that fields need to be update in db? – vernou Oct 20 '22 at 14:05
  • Since I am using EntityState.Modified, the objFromDbAct part of the code is used to make sure that no other tracking exists on that record, since EntityState.Modified will be attaching the entity to the DbContext and marking all fields for update. That part of the code is working, the record is updated correctly, my question is how to use TrackChanges while using EntityState.Modified for updates. – Francisco Souza Oct 20 '22 at 14:11
  • What do you want do with `TrackChanges`? – vernou Oct 20 '22 at 14:25
  • I want to get the .DebugView.LongView and save it on a DB to have a record of the changes made by the user, without having to compare every single field manually for every possible model. – Francisco Souza Oct 20 '22 at 14:34
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/248942/discussion-between-francisco-souza-and-vernou). – Francisco Souza Oct 20 '22 at 16:59
  • Can you show the model? Entities and context classes. – vernou Oct 21 '22 at 21:22

2 Answers2

2

You can use existing methods on DbEntityEntry to reload database values.

public void SaveCustomer(Clients currentModel)
{
    var objFromDbAct = _db.Clients.Local.FirstOrDefault(u => u.Recid == currentModel.Recid);
    if (objFromDbAct != null) { _db.Entry(objFromDbAct).State = EntityState.Detached; }

   _db.Entry(currentModel).GetDatabaseValues();
       
    _db.ChangeTracker.DetectChanges();
    string trackChanges = _db.ChangeTracker.DebugView.LongView;

    _db.SaveChanges();            
 }

Optionally, if you track only this exact entity, you may clear change tracker to simplify the code.

public void SaveCustomer(Clients currentModel)
{
    _db.ChangeTracker.Clear();

   _db.Entry(currentModel);.GetDatabaseValues();
       
    _db.ChangeTracker.DetectChanges();
    string trackChanges = _db.ChangeTracker.DebugView.LongView;

    _db.SaveChanges();            
 }

UPDATE:

I have initially missed one additional method call as GetDatabaseValues method returns values from database, but not internally setting those anywhere. Also _db.Entry(value); is adding entity into the change tracking, but in detached state, thus changes are not detected.

public void SaveCustomer(Clients currentModel)
{
    // clear tracked entries
    _db.ChangeTracker.Clear();

    // attach current model
    var entry = _db.Attach(currentModel);

    // get database values
    var originalValues = entry.GetDatabaseValues();

    // set database values as original values
    entry.OriginalValues.SetValues(originalValues);

    string trackChanges = _db.ChangeTracker.DebugView.LongView;

    _db.SaveChanges();            
 }

Testing program output

Related links:

DbEntityEntry.GetDatabaseValues Method

DbPropertyValues.SetValues Method

ChangeTracker.Clear Method

dropoutcoder
  • 2,627
  • 2
  • 14
  • 32
  • Using _db.Entry(currentModel).GetDatabaseValues() returns the database value correctly but the ChangeTracker still only sees the new model, and does not compare the changes made from the database. As if the model was created from scratch and all values added at once. – Francisco Souza Oct 20 '22 at 12:20
  • Okay, let me try over the weekend to reproduce the issue and find a way how to resolve it. – dropoutcoder Oct 21 '22 at 12:58
  • I'll be able to test it tomorrow. But from what I can see you are attaching the model which wont allow _db.Entry(currentModel).State = EntityState.Modified to run (this is how I am comparing the changes between the database and the input model) this line is crucial and I don't see it on your sample. I have edited the question so it's more clear the main problem. I'll give another feedback tomorrow – Francisco Souza Oct 22 '22 at 19:28
  • Once you call SetValues method on OriginalValues property, change tracker will run internal comparison and set only changed properties to modified. You can see it on the output. Second to last with heading GetDatabaseValuesAsync. You don't want to change the state manually. If you do then all properties are marked as modified and you are back where have you been. Try it. I think this one will work. If you run into some issues, I am happy to take a look into whole solution(if you can share) to make it work. – dropoutcoder Oct 22 '22 at 22:33
  • Yeah it works perfectly, took me a while to understand your approach, the point is to not use the entitystate.modified since it recreates the object. Thank you for the help! – Francisco Souza Oct 23 '22 at 21:10
1

It is possible to copy the entity's values from a other class instance as follows :

public void SaveCustomer(Client currentModel)
{
    var objFromDbAct = _db.Clients.Local.FirstOrDefault(c => c.Recid == currentModel.Recid);
    if (objFromDbAct == null)
    {
        _db.Update(currentModel);
    }
    else
    {
        var preEntity = _db.Entry(objFromDbAct);
        preEntity.CurrentValues.SetValues(currentModel);
    }

    _db.ChangeTracker.DetectChanges();
    string trackChanges = _db.ChangeTracker.DebugView.LongView;

    _db.SaveChanges();
}

trackChanges value :

Client {Recid: 1} Modified
  Recid: 1 PK
  Addr1: 'Address 1'
  Addr2: 'Address 2'
  Zip: 'Modified' Modified Originally 'Zip'

And the SQL executed by SaveChanges:

UPDATE [Clients] SET [Zip] = @p0
WHERE [Recid] = @p1;
vernou
  • 6,818
  • 5
  • 30
  • 58