Search code examples
c#entity-frameworksimple-injector

Intercept / Decorate Entity Framework's DbContext.SaveChanges() through Simple Injector


I am writing an AuditLog based on the changes tracked in the Context object in Entity Framework.

I need to be able to intercept calls to Context.SaveChanges(), as I then iterate through Context.ChangeTracker.Entries() and log these to my AuditLog.

I need to be able to do this on the DbContext itself as I both have the possibility to fetch a DbContext transiently directly and PerWebRequest through my UnitOfWork.

I have tried to address this through a decorator, which isn't working for me!

public class AuditLogSaveChangesInKNContextDecorator : DbContext
{
    DbContext _context;
    IHandleCommand<AddAuditLogEntriesFromTrackedChangesAndSaveChangesCommand> _handler;

    public AuditLogSaveChangesInKNContextDecorator(DbContext context,
        IHandleCommand<AddAuditLogEntriesFromTrackedChangesAndSaveChangesCommand> handler)
    {
        _context = context;
        _handler = handler;
    }

    public override int SaveChanges()
    {
        var changes = base.SaveChanges();

        _handler.Handle(new AddAuditLogEntriesFromTrackedChangesAndSaveChangesCommand { 
            Context = _context 
        });

        return changes;
    }
}

Any Ideas?


Solution

  • What you are doing seems quite right to me. The only way to 'intercept' the call to SaveChanges is by overriding that method; it is virtual. For instance:

    public class MyDbContext : DbContext
    {
        public event Action<MyDbContext> SavingChanges = _ => { };
    
        public override int SaveChanges()
        {
            // Notify objects that want to know, that we are gonna save some stuff
            this.SavingChanges(this);
    
            // Call the actual SaveChanges method to apply the changes to the database
            return base.SaveChanges();
        }
    }
    

    By using an event, we can add any behavior (dependency injection) to the MyDbContext without that context having to know about it. For instance:

    container.RegisterPerWebRequest<MyDbContext>(() =>
    {
        var context = new MyDbContext();
        context.SavingChanges += UpdateEntities;
        return context;
    });
    
    private static void UpdateEntities(MyDbContext db)
    {
        var addedEntities =
            from entry in db.ChangeTracker.Entries()
            where entry.State == EntityState.Added
            select entry.Entity as IEntity;
    
        db.AuditTrailEntries.AddRange(
            from entity in addedEntities
            select new AuditTrailEntry 
            { 
                EntityId = entity.Id, 
                Type = entity.GetType().Name 
            });
    }