Entity Framework Audit Fields

There are several articles on the internet about the automatic population of database audit fields for user tables. Good examples of such fields are Created By, Created Date, Modified By or Modified Date, although there may be many reasons to audit other value changes.

A good solution I have come across is to make the entities we want audited extend an interface, that we might call IAuditable, and override the Entity Framework Save method, setting up the audit fields for the entities that implement that same interface.

Such a code should be very similar to the following:

public override int SaveChanges() {
    foreach (var auditableEntity in ChangeTracker.Entries<IAuditatleEntity>()) {
        if (auditableEntity.State == EntityState.Added || auditableEntity.State == EntityState.Modified) {
            var currentUser = 0; //Get the user Id or Name depending on the type specified in the interface
            auditableEntity.Entity.UpdatedDate = DateTime.UtcNow;
            auditableEntity.Entity.UpdatedBy = currentUser;

            if (auditableEntity.State == EntityState.Added) {
                auditableEntity.Entity.CreatedDate = DateTime.UtcNow;
                auditableEntity.Entity.CreatedBy = currentUser;
            }
            else {
                // Make sure we don't change the created by fields in any operation that is not adding
                auditableEntity.Property(p => p.CreatedDate).IsModified = false;
                auditableEntity.Property(p => p.CreatedBy).IsModified = false;
            }
        }
    }
    return base.SaveChanges();
}

I like this solution because it works in the central place where we manage our application database access.

However, what I would like to approach in the rest of this post is how could we store this functionality and reuse it across different projects, or even combine it with other working functionality that also works at the Entity Framework Save Changes level.

As such, I want to extend this example to achieve the following:

  • Store this logic in a core library so it can be reused in several different projects;
  • Extend other DbContext functionality that might already exist; and
  • Provide an effective way for the client code to specify the desired inputs, in this case, the user ID.

This might not be overly useful for this auditing fields example, but a similar approach can be used in more complex scenarios that also works on top of an Entity Framework DbContext class.

With the approach below I achieve the goals above by creating a decorator over the DbContext that would allow the programmer to compose the behavior of the SaveChanges functionality.

As I don't want the decorators to be extending the entity framework DbContext class directly, a DbContext wrapper is created.

Decorators need to have access to the original DBContext, though, because they need to access the original DB context properties and methods, e.g. the entites that we are tracking changes on (ChangeTracker).

For the specifics of the client code, in this case the UserId specification, one can extend a particular decorator.

Although the decorator pattern is most useful when used for choosing and applying decorations in run-time, which is not the case for this particular example, it favors composition over inheritance and that can be very useful if we need combine different functionality. This solution also helps us achieve the OCP and DRY principles.

For the sake of simplicity, in this example below I have made the assumption that some of tables that we are auditing use a user id and others are using a user name for the Created By and Modified By fields.

 

The DbContext wrapper

public abstract class DbContextWrapper
        where TContext : DbContext
{
    public TContext Context { get; private set; }

    protected DbContextWrapper(TContext context) {
        Context = context;
    }

    public abstract int SaveChanges(); 
}

 

The base decorator

public class DbContextWrapperDecorator : DbContextWrapper
        where TContext : DbContext
{
    protected readonly DbContextWrapper DbContextWrapper;

    protected DbContextWrapperDecorator(DbContextWrapper contextWrapper) : base(contextWrapper.Context) {
        DbContextWrapper = contextWrapper;
    }

    public override int SaveChanges() {
        return DbContextWrapper.SaveChanges();
    }
}

 

The abstract decorator for auditing fields

public abstract class DbContextWrapperAuditableEntityDecorator : DbContextWrapperDecorator
    where TContext : DbContext
{
    protected DbContextWrapperAuditableEntityDecorator(DbContextWrapper contextWrapperDecorator) : base(contextWrapperDecorator) { }

    public override int SaveChanges() {
        foreach (var auditableEntity in Context.ChangeTracker.Entries<IAuditatleEntity>()) {
            if (auditableEntity.State == EntityState.Added || auditableEntity.State == EntityState.Modified) {
                var currentUser = GetCurrentUser(); 

                auditableEntity.Entity.UpdatedDate = DateTime.UtcNow;
                auditableEntity.Entity.UpdatedBy = currentUser;

                if (auditableEntity.State == EntityState.Added) {
                    auditableEntity.Entity.CreatedDate = DateTime.UtcNow;
                    auditableEntity.Entity.CreatedBy = currentUser;
                }
                else {
                    auditableEntity.Property(p => p.CreatedDate).IsModified = false;
                    auditableEntity.Property(p => p.CreatedBy).IsModified = false;
                }
            }
        }
        return base.SaveChanges();
    }

    protected abstract TOfUser GetCurrentUser();
}

 

The concrete decorator for integer user id auditing

public class ExampleContextWrapperIdDecorator: DbContextWrapperAuditableEntityDecorator
    where TContext : DbContext
{
    public ExampleContextWrapperIdDecorator(DbContextWrapper contextWrapper) : base(contextWrapper) { }

    protected override int GetCurrentUser() {
        return 1; //place your logic for getting the user id here
    }
}

 

The concrete decorater for string user name auditing

public class ExampleContextWrapperNameDecorator: DbContextWrapperAuditableEntityDecorator
    where TContext : DbContext
{
    public ExampleContextWrapperNameDecorator(DbContextWrapper contextWrapper) : base(contextWrapper)
    {
    }

    protected override string GetCurrentUser()
    {
        return "some user"; //place your logic for getting the user name here
    }
}

 


Nuno 11 Aug 2014