Capturing the audit data

In this recipe, we will be walking through how to add a seamless audit data to your objects with minimal effort.

Getting ready

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.

How to do it...

  1. We start be writing out a test that will allow us to define the scope of our problem. In this case, we want our context to handle the data for auditable objects without the user input. We define it with the following code:
    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);
    
        }
    
      }
    
    }
  2. We then add our 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; }
      }
    }
  3. We then add our mapping as a new C# file named 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");
        }
    
      }
    }
  4. The next step is for us to add the mapping to the context and make the context change the commit behavior with the following code:
    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);
        }
      }
    }
  5. All of our tests pass.

How it works...

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.

There's more...

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.

Created date and created by, modified date and modified by

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.

Action taken

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.

See also

In this chapter:

  • Handling soft delete
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset