Search code examples
entity-frameworkentity-framework-4ef-code-firsttrackingef4-code-only

Intercepting and logging changes with EF 4.2 code first


I spent some time reading a few posts and articles about audit tracking but still can't figure this out.

What I need is a pretty basic audit system that will give me results such as:

  • Customer "John Doe" was deleted by "User"
  • Customer "Jane Doe" was created by "Other User"
  • Address from "John Doe" was modified by "User"
  • Customer "Jane Doe" was removed by "Other User"

I tried to override the SaveChanges event on dbContext but i got stuck with the following problem:

public override int SaveChanges()
{

        foreach (DbEntityEntry<IAuditable> entry in ChangeTracker.Entries<IAuditable>())
        {
            if (entry.State == EntityState.Added)
            {
                // since the object was not added yet, if I write to log in here and
                // for some reason SaveChanges fail, I will end up with a fake log entry
            }
            else if (entry.State == EntityState.Modified)
            {
                // same in here
            }
        }

        return base.SaveChanges();

        // here the state for all entries have changed to Unchanged or Detached.
        // detached is probably the one that was deleted however the “Unchanged”
        // could be new or modified records.
}

I know I could use triggers on the database to accomplish this but I'd like to keep it here so I have more control over it, since I'm not an SQL guy and after deploy the application I won't have that much control over the db.

I'm 'sure I'm missing something very simple here. I appreciate any help.

Thanks in advance.


Solution

  • You can always check if there are any audit entries in your context and remove them when your SaveChanges is called. It will solve your first issue - you will always remove previous audit entries prior to creating a new one.

    The second problem cannot be solved with DbContext API. DbContext API is simplification of ObjectContext API and this simplification removed methods needed for complex scenarios. One such method is overloaded version of SaveChanges with ability to not accept changes (not change states of entities).

    You can convert DbContext to ObjectContext through IObjectContextAdapter. Even with ObjectContext it will not be straight forward:

    // If you want to have audits in transaction with records you must handle
    // transactions manually
    using (TransactionScope scope = new TransactionScope(...))
    {
        ObjectContext context = ((IObjectContextAdapter)this).ObjectContext;
        context.SaveChanges(SaveOptions.DetectChangesBeforeSave);
    
        var audits = new List<Audit>();
    
        // Now you must call your audit code but instead of adding audits to context
        // you must add them to list. 
    
        // This is the reason why you must not add changes to context. You must accept
        // old changes prior to adding your new audit records otherwise EF will perform
        // changes again. If you add your entities to context and call accept before 
        // saving them your changes will be lost
        context.AcceptAllChanges();
    
        // Now add all audits from list to context
    
        context.SaveChanges();
    
        // Complete the transaction
        scope.Complete(); 
    }