0

I'm developing an ASP.NET Core project where in SaveChanges, I need to log the updated values

According to this I tried to override SaveChangesAsync but I get this error :

Unable to cast object of type 'MyProject.ApplicationDbContext' to type 'System.Data.Entity.Infrastructure.IObjectContextAdapter'.

Code:

public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
    ChangeTracker.DetectChanges();

    ObjectContext ctx = ((IObjectContextAdapter)this).ObjectContext;

    List<ObjectStateEntry> objectStateEntryList = ctx.ObjectStateManager
                                                     .GetObjectStateEntries((System.Data.Entity.EntityState)(EntityState.Added | EntityState.Modified | EntityState.Deleted))
                                                     .ToList();

    foreach (ObjectStateEntry entry in objectStateEntryList)
    {
        if (!entry.IsRelationship)
        {
            switch (entry.State)
            {
                case (System.Data.Entity.EntityState)EntityState.Added:
                    // write log...
                    break;

                case (System.Data.Entity.EntityState)EntityState.Deleted:
                    // write log...
                    break;

                case (System.Data.Entity.EntityState)EntityState.Modified:
                    foreach (string propertyName in entry.GetModifiedProperties())
                    {
                        DbDataRecord original = entry.OriginalValues;
                        string oldValue = original.GetValue(original.GetOrdinal(propertyName)).ToString();

                        CurrentValueRecord current = entry.CurrentValues;
                        string newValue = current.GetValue(current.GetOrdinal(propertyName)).ToString();

                        if (oldValue != newValue) // probably not necessary
                        {
                            var a = string.Format("Entry: {0} Original :{1} New: {2}",
                                                  entry.Entity.GetType().Name,
                                                  oldValue, newValue);
                        }
                    }
                    break;
           }
       }
   }

   return base.SaveChangesAsync();
}

Also I tried to compose the log string in the Action method but I get the same error.

public async Task<IActionResult> Edit(MyModel model)
{
    ...
    // _context is ApplicationDbContext()
    var myObjectState = _context.ObjectStateManager.GetObjectStateEntry(model);
    var modifiedProperties = myObjectState.GetModifiedProperties();

    foreach (var propName in modifiedProperties)
    {
        Console.WriteLine("Property {0} changed from {1} to {2}",
                          propName,
                          myObjectState.OriginalValues[propName],
                          myObjectState.CurrentValues[propName]);
    }
}

I'm using EF 6.

I need to log the property name, old value and new value. I can't get why I'm getting that error, what am I doing wrong?

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
lgica
  • 21
  • 1
  • 6

1 Answers1

2

what am I doing wrong?

Using an example from EF 4.1? :)

You can utilize the ChangeTracker in EF 6 to accomplish what you are looking for. I would recommend setting up a separate DbContext for your logging containing just the logging table and FKs as needed:

Within your SaveChanges/SaveChangesAsync overrides:

var updatedEntities = ChangeTracker.Entries()
    .Where(x => x.State == EntityState.Modified);

var insertedEntities = ChangeTracker.Entries()
    .Where(x => x.State == EntityState.Added);

From here you can access the original and modified property values through OriginalValues and CurrentValues respectively.

** Update ** When reporting on changes the typical easy way is to use ToObject() on the OriginalValues and CurrentValues then serialize to something like Json to capture the before and after state of the object in question. However this will display all modified and unmodified values.

To iterate through the values to find actual changes is a bit more involved:

foreach(var entity in updatedEntities)
{
    foreach (var propertyName in entity.OriginalValues.PropertyNames)
    {
        if (!object.Equals(entity.OriginalValues.GetValue<object>(propertyName), 
        entity.CurrentValues.GetValue<object>(propertyName)))
        {
            var columnName = propertyName;
            var originalValue = entity.OriginalValues.GetValue<object>(propertyName)?.ToString() ?? "#null";
            var updatedValue = entity.CurrentValues.GetValue<object>(propertyName)?.ToString() ?? "#null";
            var message = $"The prop: {columnName} has been changed from {originalValue} to {updatedValue}";
        }
    }
}
Steve Py
  • 26,149
  • 3
  • 25
  • 43
  • How can I get the proprty name, old value and new value? I made a foreach loop where for each entity I loop through and I'm trying to compose a string like this : `string.Format("The prop: {0} has been changed from {1} to {2}", prop.ToString(), prop.OriginalValues, prop.CurrentValues);` – lgica May 24 '21 at 07:16
  • I have expanded on the answer to provide options for generating a change history. – Steve Py May 24 '21 at 22:26
  • OriginalValues does not contains PropertyNames. I already tried this way. What happens if pass whole object to `context.Update(objectToUpdate)` ? - OriginalValues and CurrentValues will be the same – lgica May 24 '21 at 22:36
  • 1
    ? I'm using EF 6 and I can access PropertyNames off of OriginalValues. Make sure you're using `var updatedEntities = ChangeTracker.Entries().Where(x => x.State == EntityState.Modified);` .. If you are using EF Core rather than EF 6 then OriginalValues has `Properties` which are objects that contain the `.Name` property. I do not recommend using `context.Update(objectToUpdate)`, as this generates an UPDATE statement for all columns whether they change or not. If you want to track updates it is better to let EF change tracker manage it. – Steve Py May 24 '21 at 23:04