35

I'm using EF code first. I'm using a base Repository for all my repositories and an IUnitofWork that inject to the repositories, too:

public interface IUnitOfWork : IDisposable
{
    IDbSet<TEntity> Set<TEntity>() where TEntity : class;
    int SaveChanges();
}

public class BaseRepository<T> where T : class
{
    protected readonly DbContext _dbContext;
    protected readonly IDbSet<T> _dbSet;


    public BaseRepository(IUnitOfWork uow)
    {
        _dbContext = (DbContext)uow;
        _dbSet = uow.Set<T>();
    }
    //other methods
}   

e.g my OrderRepository is like this:

class OrderRepository: BaseRepository<Order>
{
    IUnitOfWork _uow;
    IDbSet<Order> _order;

    public OrderRepository(IUnitOfWork uow)
        : base(uow)
    {
        _uow = uow;
        _order = _uow.Set<Order>();
    }
    //other methods
}

And I use it in this way:

public void Save(Order order)
{
        using (IUnitOfWork uow = new MyDBContext())
        {
            OrderRepository repository = new OrderRepository(uow); 
            try
            {
               repository.ApplyChanges<Order>(order);    
               uow.SaveChanges();
            }  

        } 
}     

Is there any way to log change histories of all entities(include their navigation properties) during .SaveChanges()? I want to log original values(before save occurs) and changed values(after save occurs).

Masoud
  • 8,020
  • 12
  • 62
  • 123
  • What do you mean by "include their navigation properties"? That could potentially result in large parts of your database being written to your log... – Steve Ruble Aug 03 '13 at 19:03
  • Also, can you be more precise about what changes you want to log? The way it's worded now looks like you're only interested in changes made *during* the save (i.e., IDENTITY columns and timestamps) but I suspect that you mean to log values from before and after `repository.ApplyChanges`. Is that correct? – Steve Ruble Aug 03 '13 at 19:24
  • @SteveRuble: yeah, I mean to log values from befor and after, also for "include their navigation properties" e.g when I'm saving an Order, i want to log Order.Customer.Name value before and after save. – Masoud Aug 04 '13 at 07:30

5 Answers5

64

You can get the before and after values for all changed entities by going through DbContext.ChangeTracker. Unfortunately the API is a little verbose:

var changeInfo = context.ChangeTracker.Entries()
            .Where (t => t.State == EntityState.Modified)
            .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]),
            });

You can modify that to include things like the type of the entity if you need that for your logging. There is also a ToObject() method on the DbPropertyValues (the type of OriginalValues and CurrentValues) you could call if you already have a way to log whole objects, although the objects returned from that method will not have their navigation properties populated.

You can also modify that code to get all entities in the context by taking out the Where clause, if that makes more sense given your requirements.

Steve Ruble
  • 3,875
  • 21
  • 27
  • 2
    Thanks, as you said "the objects returned from that method will not have their navigation properties populated", but navigation properties are important to me, do you know any way to accomplish this? – Masoud Aug 05 '13 at 06:19
  • 1
    @Masoud, is it important to have the navigation properties logged like "Order.Customer.Name=Value" or is it enough to just have the value of Customer.Name included in the information logged? If you want the first option, you're going to need to write something really complicated to serialize your entities in the format you want; if you want the second, my example should work for you. – Steve Ruble Aug 05 '13 at 10:47
  • Couldn't you just attach the objects returned from ToObject to a context and let the context fix up all the entity relationships? – sjb-sjb Apr 04 '18 at 12:01
11

I have overridded the default SaveChanges method to log changes for add/update/delete in entity. Though it does not cover navigation property changes.
Based on this article: Using entity framework for auditing

public int SaveChanges(string userId)
    {
        int objectsCount;

        List<DbEntityEntry> newEntities = new List<DbEntityEntry>();

        // Get all Added/Deleted/Modified entities (not Unmodified or Detached)
        foreach (var entry in this.ChangeTracker.Entries().Where
            (x => (x.State == System.Data.EntityState.Added) ||
                (x.State == System.Data.EntityState.Deleted) ||
                (x.State == System.Data.EntityState.Modified)))
        {
            if (entry.State == System.Data.EntityState.Added)
            {
                newEntities.Add(entry);
            }
            else
            {
                // For each changed record, get the audit record entries and add them
                foreach (AuditLog changeDescription in GetAuditRecordsForEntity(entry, userId))
                {
                    this.AuditLogs.Add(changeDescription);
                }
            }
        }

        // Default save changes call to actually save changes to the database
        objectsCount = base.SaveChanges();

        // We don't have recordId for insert statements that's why we need to call this method again.
        foreach (var entry in newEntities)
        {
            // For each changed record, get the audit record entries and add them
            foreach (AuditLog changeDescription in GetAuditRecordsForEntity(entry, userId, true))
            {
                this.AuditLogs.Add(changeDescription);
            }

            // TODO: Think about performance here. We are calling db twice for one insertion.
            objectsCount += base.SaveChanges();
        }

        return objectsCount;
    }

    #endregion

    #region Helper Methods

    /// <summary>
    /// Helper method to create record description for Audit table based on operation done on dbEntity
    /// - Insert, Delete, Update
    /// </summary>
    /// <param name="dbEntity"></param>
    /// <param name="userId"></param>
    /// <returns></returns>
    private List<AuditLog> GetAuditRecordsForEntity(DbEntityEntry dbEntity, string userId, bool insertSpecial = false)
    {
        List<AuditLog> changesCollection = new List<AuditLog>();

        DateTime changeTime = DateTime.Now;

        // Get Entity Type Name.
        string tableName1 = dbEntity.GetTableName();

        // http://stackoverflow.com/questions/2281972/how-to-get-a-list-of-properties-with-a-given-attribute
        // Get primary key value (If we have more than one key column, this will need to be adjusted)
        string primaryKeyName = dbEntity.GetAuditRecordKeyName();

        int primaryKeyId = 0;
        object primaryKeyValue;

        if (dbEntity.State == System.Data.EntityState.Added || insertSpecial)
        {
            primaryKeyValue = dbEntity.GetPropertyValue(primaryKeyName, true);

            if(primaryKeyValue != null)
            {
                Int32.TryParse(primaryKeyValue.ToString(), out primaryKeyId);
            }                

            // For Inserts, just add the whole record
            // If the dbEntity implements IDescribableEntity,
            // use the description from Describe(), otherwise use ToString()
            changesCollection.Add(new AuditLog()
                    {
                        UserId = userId,
                        EventDate = changeTime,
                        EventType = ModelConstants.UPDATE_TYPE_ADD,
                        TableName = tableName1,
                        RecordId = primaryKeyId,  // Again, adjust this if you have a multi-column key
                        ColumnName = "ALL",    // To show all column names have been changed
                        NewValue = (dbEntity.CurrentValues.ToObject() is IAuditableEntity) ?
                                        (dbEntity.CurrentValues.ToObject() as IAuditableEntity).Describe() :
                                        dbEntity.CurrentValues.ToObject().ToString()
                    }
                );
        }

        else if (dbEntity.State == System.Data.EntityState.Deleted)
        {
            primaryKeyValue = dbEntity.GetPropertyValue(primaryKeyName);

            if (primaryKeyValue != null)
            {
                Int32.TryParse(primaryKeyValue.ToString(), out primaryKeyId);
            }

            // With deletes use whole record and get description from Describe() or ToString()
            changesCollection.Add(new AuditLog()
                    {
                        UserId = userId,
                        EventDate = changeTime,
                        EventType = ModelConstants.UPDATE_TYPE_DELETE,
                        TableName = tableName1,
                        RecordId = primaryKeyId,
                        ColumnName = "ALL",
                        OriginalValue = (dbEntity.OriginalValues.ToObject() is IAuditableEntity) ?
                                    (dbEntity.OriginalValues.ToObject() as IAuditableEntity).Describe() :
                                    dbEntity.OriginalValues.ToObject().ToString()
                    });
        }

        else if (dbEntity.State == System.Data.EntityState.Modified)
        {
            primaryKeyValue = dbEntity.GetPropertyValue(primaryKeyName);

            if (primaryKeyValue != null)
            {
                Int32.TryParse(primaryKeyValue.ToString(), out primaryKeyId);
            }

            foreach (string propertyName in dbEntity.OriginalValues.PropertyNames)
            {
                // For updates, we only want to capture the columns that actually changed
                if (!object.Equals(dbEntity.OriginalValues.GetValue<object>(propertyName),
                        dbEntity.CurrentValues.GetValue<object>(propertyName)))
                {
                    changesCollection.Add(new AuditLog()
                    {
                        UserId = userId,
                        EventDate = changeTime,
                        EventType = ModelConstants.UPDATE_TYPE_MODIFY,
                        TableName = tableName1,
                        RecordId = primaryKeyId,
                        ColumnName = propertyName,
                        OriginalValue = dbEntity.OriginalValues.GetValue<object>(propertyName) == null ? null : dbEntity.OriginalValues.GetValue<object>(propertyName).ToString(),
                        NewValue = dbEntity.CurrentValues.GetValue<object>(propertyName) == null ? null : dbEntity.CurrentValues.GetValue<object>(propertyName).ToString()
                    }
                        );
                }
            }
        }


        // Otherwise, don't do anything, we don't care about Unchanged or Detached entities
        return changesCollection;
    }
Rohit
  • 6,365
  • 14
  • 59
  • 90
  • What do you mean it doesnt cover navigation property changes? EF tracks changes to all objects. Please clarify. – Shiva Naru Mar 17 '16 at 00:10
8

you have scared people away with the extra requirement

Include their navigation properties

This is simply a non trivial exercise. And if this is important, you should manage/track changes across references with code.

this is a sample covering this topic Undo changes in entity framework entities

There is a sample doing close top what you want here undo changes It can easily be converted to load before and after images elsewhere.

Given the ObjectState entry after DetectChanges is called, you can implement a simple entity by entity option. and per UOW. But the navigation / references version makes this very complex as you worded the requirement.

EDIT : How to access the changeList

     public class  Repository<TPoco>{
     /....
     public DbEntityEntry<T> Entry(T entity) { return Context.Entry(entity); }

     public virtual IList<ChangePair> GetChanges(object poco) {

        var changes = new List<ObjectPair>();
        var thePoco = (TPoco) poco;

        foreach (var propName in Entry(thePoco).CurrentValues.PropertyNames) {
            var curr = Entry(thePoco).CurrentValues[propName];
            var orig = Entry(thePoco).OriginalValues[propName];
            if (curr != null && orig != null) {
                if (curr.Equals(orig)) {
                    continue;
                }
            }
            if (curr == null && orig == null) {
                continue;
            }
            var aChangePair = new ChangePair {Key = propName, Current = curr, Original = orig};
            changes.Add(aChangePair);
        }
        return changes;
    }
    ///...  partial repository shown
    } 
// FYI the simple return structure

public class ChangePair {
    public string Key { get; set; }
    public object Original { get; set; }
    public object Current { get; set; }
 }
Community
  • 1
  • 1
phil soady
  • 11,043
  • 5
  • 50
  • 95
  • Thanks, phil, but your suggested solution assume that I have the property name, when-as I want log changes with all properties of all entities, though i think using Reflection, I could do it, but Reflection has efficiency problem(low speed). – Masoud Aug 04 '13 at 07:32
4

DbContext has ChangeTracker property. You can override .SaveChanges() in your context and log changes. I don't think that entity framework can do it for you. Probably, you must detect changes directly in your model classes.

Roman Bats
  • 1,775
  • 3
  • 30
  • 40
0

I've expanded on Steve's answer to provide a check for Changed, Added, and Deleted entities and print them in a sensible way.

(My use case is to ensure there are no unsaved changes before disposing of a DbContext instance, but this check could be done at any point)


/// <summary>Helper method that checks whether the DbContext had any unsaved changes before it was disposed.</summary>
private void CheckForUnsavedChanges(DbContext dbContext)
{
    try
    {
        List<DbEntityEntry> changedEntityEntries = dbContext.ChangeTracker.Entries()
            .Where(t => t.State != EntityState.Unchanged && t.State != EntityState.Detached).ToList();
        if (!changedEntityEntries.Any())
            return;
        throw new Exception("Detected that there were unsaved changes made using a DbContext. This could be due to a missing call to `.SaveChanges()` or possibly " +
            "some read-only operations that modified the returned entities (in which case you might wish to use `.AsNoTracking()` in your query). Changes:\n    " +
            String.Join("\n    ", changedEntityEntries.Select(entry => $"{entry.Entity.GetType()} {entry.State}:\n        " + String.Join("\n        ",
            entry.State == EntityState.Modified ? entry.CurrentValues.PropertyNames
                // Only output properties whose values have changed (and hope they have a good ToString() implementation) 
                .Where(pn => entry.OriginalValues?[pn] != entry.CurrentValues[pn])
                .Select(pn => $"{pn} ({entry.OriginalValues?[pn]} -> {entry.CurrentValues[pn]})") :
            // Added or Deleted entities are output in their entirety
            entry.State == EntityState.Added ? entry.CurrentValues.PropertyNames.Select(pn => $"{pn} = {entry.CurrentValues[pn]}") :
         /* entry.State == EntityState.Deleted ? */ entry.CurrentValues.PropertyNames.Select(pn => $"{pn} = {entry.OriginalValues[pn]}")))));
    }
    catch (Exception ex)
    {
        _logger.Error("Issue encountered when checking for unsaved changes.", ex);
    }
}
Alain
  • 26,663
  • 20
  • 114
  • 184