In this recipe, we will be walking through how to add a seamless audit data to your objects with minimal effort.
We will be using the NuGet
Package Manager to install the Entity Framework 4.1 assemblies.
The package installer can be found at http://nuget.codeplex.com/.
We will also be using a database for connecting to the data, and updating it.
Open the Improving Audit Data solution in the included source code examples.
using System; using System.Linq; using BusinessLogic; using DataAccess; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Test { [TestClass] public class QueryTests { [TestMethod] public void ShouldAddAuditDataWithoutUserInteraction() { var blog = new Blog() { Title = "Test", Rating = 4, ShortDescription = "Testing" }; var repo = new BlogRepository(new BlogContext()); repo.Add(blog); repo.SaveChanges(); Assert.IsNotNull(blog.ModifiedDate); Assert.IsNotNull(blog.ModifiedBy); Assert.IsNotNull(blog.Creationdate); Assert.IsNotNull(blog.CreatedBy); } } }
Blog
object and it's inheritance chain as a new C# file named Blog.cs
to our BusinessLogic
project, so we have an example object to connect to, with the following code:using System; namespace BusinessLogic { public class Blog : AuditableEntity { public string ShortDescription { get; set; } public string Title { get; set; } public double Rating { get; set; } } public class AuditableEntity : Entity { public string ModifiedBy { get; set; } public DateTime ModifiedDate { get; set; } public string CreatedBy { get; set; } public DateTime Creationdate { get; set; } } public class Entity { public int Id { get; set; } } }
BlogMapping
to the DataAccess
project, with the following code:using System.ComponentModel.DataAnnotations; using System.Data.Entity.ModelConfiguration; using BusinessLogic; namespace DataAccess.Mappings { public class BlogMapping : EntityTypeConfiguration<Blog> { public BlogMapping() { this.ToTable("Blogs"); this.HasKey(x => x.Id); this.Property(x => x.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity).HasColumnName("BlogId"); this.Property(x => x.Title).IsRequired().HasMaxLength(250); this.Property(x => x.Creationdate).HasColumnName("CreationDate").IsRequired(); this.Property(x => x.ShortDescription).HasColumnType("Text").IsMaxLength().IsOptional().HasColumnName("Description"); } } }
using System; using System.Collections.Generic; using System.Data; using System.Data.Entity; using System.Data.Entity.Infrastructure; using System.Linq; using BusinessLogic; using DataAccess.Mappings; using Isg.EntityFramework.Interceptors; namespace DataAccess { public class BlogContext : InterceptorDbContext, IUnitOfWork { public void Add<T>(T entity) where T : class { this.Set<T>().Add(entity); } public void Commit() { var addedItems = this.ChangeTracker.Entries().Where(x => x.State == EntityState.Added && x.Entity is AuditableEntity); var modifiedItems = this.ChangeTracker.Entries().Where(x => x.State == EntityState.Modified && x.Entity is AuditableEntity); AttachAuditDataForInserts(addedItems); AttachAuditDataForModifications(modifiedItems); SaveChanges(); } private void AttachAuditDataForModifications(IEnumerable<DbEntityEntry> modifiedItems) { modifiedItems.Each(x => { var auditableEntity = (AuditableEntity) x.Entity; auditableEntity.ModifiedBy = "UserName"; auditableEntity.ModifiedDate = DateTime.Today; }); } private void AttachAuditDataForInserts(IEnumerable<DbEntityEntry> addedItems) { addedItems.Each(x => { var auditableEntity = (AuditableEntity) x.Entity; auditableEntity.CreatedBy = "UserName"; auditableEntity.Creationdate = DateTime.Today; auditableEntity.ModifiedBy = "UserName"; auditableEntity.ModifiedDate = DateTime.Today; }); } public IQueryable<T> Find<T>() where T : class { return this.Set<T>(); } public void Refresh() { this.ChangeTracker.Entries().ToList().ForEach(x => x.Reload()); } public void Remove<T>(T entity) where T : class { this.Set<T>().Remove(entity); } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Configurations.Add(new BlogMapping()); base.OnModelCreating(modelBuilder); } } }
We start with a test that defines how we want the audit data to get attached to the object. We want the objects to get saved, and the data to be supplied automatically, so users are ignorant of the interaction.
We define our Blog
object as an AuditableEntity
, so that the properties we need are defined in one spot, and we can leverage them in any object we need. We could do this in an interface if we wanted to avoid inheritance.
The mapping doesn't change drastically for this, as the fields are mapped within the standard. We then attached the object and it's mapping to the context.
The context is where we override the SaveChanges
method and use the change tracker to find objects that were added or modified. We then modify those objects with the audit data just before saving them to the database. This gives us the accurate modification times, and also allows us to separate our object modification from the audit data attachment.
When we look at most of the databases, there are several pieces of data that we want to collect, but want the user to have very little interaction with.
These fields are fairly standard and allow us to see what is happening at a high-level in the database, without having to parse application logs and look for errors. This gives us the answer to Who is creating/modifying the objects view.
If we need more data, we can leverage the change tracker to pull what properties on the entity were modified and what their current value versus original value is, so that we can log this data off, or save it to the database. This will give the Recreation or a point in time view of the data. This is value for fault-tolerant systems, and systems where a single change in the middle of a change may need to be rolled back, and the rest reapplied.