11

I am trying to implement an AuditLog using EF 4.1, by overriding the SaveChanges() method as discussed in the following places:

I am having problems with the "modified" entries though. Whenever I attempt to get at the OriginalValue of the property in question, it always has the same value as it does in the CurrentValue field.

I first use this code, and it successfully identifies the Entries that are modified:

public int SaveChanges(string userID)
{

    // Have tried both with and without the following line, and received same results:
    // ChangeTracker.DetectChanges();

    foreach (
      var ent in this.ChangeTracker
                     .Entries()
                     .Where( p => p.State == System.Data.EntityState.Added ||
                                     p.State == System.Data.EntityState.Deleted ||
                                     p.State == System.Data.EntityState.Modified ))
    {
        // For each change record, get the audit record entries and add them
        foreach (AuditLog log in GetAuditRecordsForChange(ent, userID))
        {
            this.AuditLog.Add(log);
        }

    }

    return base.SaveChanges();
}

The problem is in this (abbreviated code):

    private List<AuditLog> GetAuditRecordsForChange(DbEntityEntry dbEntry, string userID)
    {
        if (dbEntry.State == System.Data.EntityState.Modified)
        {
            foreach (string propertyName in dbEntry.OriginalValues.PropertyNames)
            {
                if (!object.Equals(dbEntry.OriginalValues.GetValue<object>(propertyName),
                    dbEntry.CurrentValues.GetValue<object>(propertyName)))
                {
                        // It never makes it into this if block, even when
                        //    the property has been updated.
                }

                // If I updated the property "Name" which was originally "OldName" to the value "NewName" and then break here and inspect the values by calling:
                //      ?dbEntry.OriginalValues.GetValue<object>("Name").ToString()

                // the result will be "NewName" and not "OldName" as expected
             }
         }
    }

The strange thing is that the call to dbEntry.Property(propertyName).IsModified(); will return true in this case. It is just that the OriginalValue doesn't have the expected value inside. Would anyone be willing to help point me in the right direction? I cannot seem to get this to work correctly.

marklark
  • 860
  • 1
  • 8
  • 18
Joe DePung
  • 991
  • 3
  • 11
  • 21
  • How are you querying your entities and then changing property values? If you are essentially attaching the entity and then setting the state to Modified, then the original values will have been lost. To keep the original values you either need to have EF track the entity all the way from query until save, or you need to keep track of the original values in your own code. – Arthur Vickers Mar 06 '12 at 18:11
  • Sorry - I tried to post some code in the comments but it wasn't working well. I'm using an [HttpPost] action of an MVC controller. This calls the "SaveProduct" method of my product repository. In the repository it looks like I do indeed call `context.Entry(product).State = EntityState.Modified` and then I SaveChanges on the context. Would you be able to point me to any resources that demonstrate the two techniques you mentioned? Or at least give me a few pointers? – Joe DePung Mar 06 '12 at 18:25
  • I _think_ in MVC the recommended way to do this would be with hidden fields. Essentially you would save the original values you care about into hidden fields and then read them back later in your POST. I don't know the best practices around this or if there is an MVC abstraction to help. – Arthur Vickers Mar 06 '12 at 18:40
  • In particular, I would like to know figure out how to "have EF track the entity all the way from query until save". I thought that was already occurring. When I just investigated a little further I found that in the repository, just before I call **context.Entry(product).State = EntityState.Modified**, I can see the original value ("OldValue"). However, as soon as I call that line of code the 'product' of interest in the DBContext has its `OriginalValue` and `CurrentValue` set to "NewValue". So, I see that you are correct, but I do not know how to update it in a way that EF will track. – Joe DePung Mar 06 '12 at 18:46
  • I didn't see your second post before my latest comment. I really appreciate your help on this. Do you think I should create another SO question now on the preferred way to do this in MVC? – Joe DePung Mar 06 '12 at 18:52
  • Sure, creating another question might be the best way to get an answer. There are probably examples on the Interweb somewhere as well. – Arthur Vickers Mar 06 '12 at 19:18
  • @ajcvickers: Can you please conclude your comments into answer because you described why original values don't work as expected. – Ladislav Mrnka Mar 06 '12 at 19:31

6 Answers6

18

When EF retrieves an entity from the database it takes a snapshot of the original values for all properties of that entity. Later, as changes are made to the values of these properties the original values will remain the same while the current values change.

However, for this to happen EF needs to be tracking the entity throughout the process. In a web or other n-tier application, typically the values are sent to the client and the context used to query the entity is disposed. This means that the entity is now no longer being tracked by EF. This is fine and good practice.

Once the application posts back the entity is reconstructed using values from the client and then re-attached to the context and set into a Modified state. However, by default the only values that come back from the client are the current values. The original values are lost. Usually this doesn't matter unless you are doing optimistic concurrency or want to be very careful about only updating values that have really changed. In these cases the original values should also be sent to the client (usually as hidden fields in a web app) and then re-applied as the original values as a part of the attach process. This was not happening in the example above and this is why the original values were not showing as expected.

mkataja
  • 970
  • 2
  • 10
  • 28
Arthur Vickers
  • 7,503
  • 32
  • 26
  • Thanks again for your help. I've got it working as expected now... I think. Just for clarification: Are you saying that calling **context.Entry(product).State = EntityState.Modified** re-attaches 'product' to the context and therefore it loses track of its OriginalValues? Also, [here](http://stackoverflow.com/questions/9591165/ef-4-how-to-properly-update-object-in-dbcontext-using-mvc-with-repository-patte) is the new post I made for my follow up (just for reference for others). – Joe DePung Mar 06 '12 at 21:15
  • But why is the approach working in the Change Tracking Example linked in @JoeDePung question? – Jensen Nov 23 '15 at 12:36
  • He is overwriting SaveChanges() and I assume he calls it before returning the data to the client and before disposing of the context. – user2555515 Aug 31 '19 at 17:11
17

If you change

dbEntry.OriginalValues.GetValue<object>(propertyName);

to

dbEntry.GetDatabaseValues().GetValue<object>(propertyName);

then that works.

Ricardo Fontana
  • 4,583
  • 1
  • 21
  • 32
Mike Munro
  • 171
  • 4
  • 4
    Works, but presumably very expensive for auditing purposes. – eoleary Jan 31 '13 at 22:28
  • 1
    Why very expansive? If you want to know the value before and after the change you have to query the database, no way. – sintetico82 Oct 08 '15 at 10:44
  • it works.. but what is the reason the first one does not hold the original value and why the same as currentvalue @eoleary – Abi May 31 '17 at 08:46
0

I got this error when i override SaveChanges in context As follows

public override int SaveChanges()
    {
        var changeInfo = ChangeTracker.Entries()
            .Select(t => new {
                Original = t.OriginalValues.PropertyNames.ToDictionary(pn => pn, pn => t.OriginalValues[pn]),
                Current = t.CurrentValues.PropertyNames.ToDictionary(pn => pn, pn => t.CurrentValues[pn]),
            }).ToList();
        return base.SaveChanges();
    }

and when I cleared it fixed!

ChangeTracker.Entries().ToList() in SaveChanges is wrong...

pejman
  • 740
  • 7
  • 13
0

The problem is not in the code you show here. The issue is that how you track entities. If you just create an entity object and calls Update on it EF framework just overwrite the existing value in db ( provided you supplied correct ID ). That is done for efficiency. So if you do:

var company = new Company{Id = mySuppliedId, Name = newname};
Context.Companies.Update(company);
Context.SaveChanges(); 

EF will go directly to DB in one shot and update all properties on the entity, without bringing anything back first. So it has no way of knowing the original values. If you change the code in your logic to something like:

var company = Context.Companies.Where(c=>c.Id == mySuppliedId).FirstOrDefault();
company.Name = newName;
Context.SaveChanges() 

Then your ChangeTracker code you showed above all of sudden starts working, as EF brought the data from DB first. It is however less efficient as you make and extra query.

user2555515
  • 779
  • 6
  • 20
0

I need the old/original value in post method. Finally this worked for me.

//Get Orignal value before save changes
      Item entityBeforeChange = db.Items.Single(x => x.Id == item.Id);
      db.Entry(entityBeforeChange).State = EntityState.Detached; // breaks up the connection to the Context
      var locId = entityBeforeChange.LocationId;//Orignal value

      //Saving the current Value
      if (ModelState.IsValid)
      {
        db.Entry(item).State = EntityState.Modified;
        await db.SaveChangesAsync();
        return RedirectToAction("Index");
      }
Waqas Javaid
  • 155
  • 1
  • 10
-1

You can get data that you haven't committed yet.

var Current = _dbContext.Entry(entity).GetDatabaseValues().ToObject();
Caner
  • 813
  • 1
  • 12
  • 26