Entity Framework Trigger Like Auditing

Background

Our stack consisted of a status Quo SOA suite combining mostly Microsoft technologies, ASP.NET, WCF, and Entity Framework.  The primary purpose of the application was a custom content management suite; the users were performing basic CRUD with a dash of business logic.

Architects decided that the enterprise needed a more scalable, generic, and "complex" security pattern, notice the lack of any adjectives relating to "security" .  The resulting configuration was the user logs into the UI using windows authentication, and then request a  SAML token from a security service.  Once the SAML was obtained, it was then passed around for all service calls.  The damaging side effect was that all of our WCF Business services ran anonymously so all auditing information could not be DB triggered, and was handled programmatically.

Data Model Overview:

Entity Framework 3.5 SP1 is more than an ORM so we created a conceptual model of our database.  Pretty much all of our tables were similar to the below,  some type of data, and then some auditing fields.

EF_Example_DataModel EF_Example_EF_Model 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Example of existing Auditing Logic:

Then there was the Update case, which was completely protected from user error….

static internal ComponentCreate(ComponentVO source, EFContext context)
{
  ComponentTypecpntType = ComponentTypeDAO.GetC(source.BusinessKey.ComponentType);
  Componenttarget = new Component()
  {
    Code = source.BusinessKey.Code,
    Description = source.Description,
    Name = source.Name,
    ComponentType= cpntType,
    PackageIndicator = false,
    InsertedBy = source.InsertedBy,
    InsertedOn = DateTime.Now,
    UpdatedBy = source.UpdatedBy,
    UpdatedOn = DateTime.Now,
  };
  returntarget;

}

Ironically enough the code sample I stole this from was setting the Insert during an update case….

Developing the new Solution

I was reading online about text templeting, and decided that this was potentially a convenient way to generate our code triggers. Three great resources are:

http://blogs.msdn.com/b/dsimmons/archive/2008/10/27/using-t4-templates-to-generate-ef-classes.aspx

http://www.hanselman.com/blog/T4TextTemplateTransformationToolkitCodeGenerationBestKeptVisualStudioSecret.aspx

http://msdn.microsoft.com/en-us/library/bb126445.aspx

All we needed to do was standardize our conceptual model, and make sure all tables with auditable fields were mapped to the same 4 property names. Once we had this, T4 gave us the ability to generate our own custom EF entities.

How it was done…

Inside the EF model we mapped all of the table audit fields to a standard set of properties. E.G. the INSRT_DT field in the database became an InsertedOn property in the EF model.

By standardizing all of the audit information it allowed us to be able to identify any entity that was “auditable.” Once we found this pattern we turned to T4 templates. Visual Studio built in support for T4 templates is coming in 2010, but I actually found out that you can still use them in 2008 SP1. With that being said let’s start.

The first thing we did was extract an IAuditable interface.

namespace Audit
{
  public interface IAuditable
  {
    voidSetInsertedOn(DateTime date);
    voidSetInsertedBy(string user);
    voidSetUpdatedOn(DateTime date);
    voidSetUpdatedBy(string user);
  }

}

Next we then detached our EF model from the built in code generation. We did this by going into the solution explorer, finding our EF model and then going to properties. There is a property on the model called “Custom Tool” and an associated value of EntityModelCodeGenerator. Simply just remove this value. In an uber high level the custom tool iterates over your entitymodel and generats the EFModel.Designer file. We are going to replace this process with a t4 template.

I obtained my based template from the following site:

http://blogs.msdn.com/b/dsimmons/archive/2008/10/27/using-t4-templates-to-generate-ef-classes.aspx

Download the template and add it to the solution. Once it’s added navigate the file and once again go to the Custom Tool property and set the value to “TextTemplatingFileGenerator.”

Now onto the messy stuff, I am actually going to show you in reverse, because the T4 stuff is absolutley hiddeous (unless you love perl, and if you love perl you have issues).

The first step in our process is to register an event handler to slightly modify the entity before saving to the databse. Think of it as a code trigger. We do this in the constructor of our Entity Framrwork Model. My model was called “EFContext” so the resulting constructor is below.

public EFContext() : base("name=EFContext", "EFContext")
{
  this.OnContextCreated();
  this.SavingChanges += new EventHandler(UpdateAuditInformation);
}

The next piece to the puzzle is to attach and implement the IAuditable interface to any entity that contains all 4 of our auditing fields. Also note we inserted an implementation that sets our fields.

Example is below:

public partial class ComponentType : EntityObject, IAuditable
{
  //UGLY EF CRAP HERE...

   ….

  #region IAuditable Members

   public void SetInsertedOn(DateTimedate)
   {
     this.InsertedOn = date;
   }

   public void SetInsertedBy(stringuser)
   {
       this.InsertedBy = user;
   }

    public void SetUpdatedOn(DateTimedate)
    {
       this.UpdatedOn = date;
    }
    public void SetUpdatedBy(stringuser)
    {
        this.UpdatedBy = user;
    }
    #endregion

}




And the final piece is the code trigger. The following code basically says, iterate through everything that has changed, if the entity is IAuditable update the trigger fields.

// SavingChanges event handler. 

private void UpdateAuditInformation(objectsender, EventArgs e)
{
    //Update create
    foreach (ObjectStateEntry entry in this.ObjectStateManager.GetObjectStateEntries(EntityState.Added | EntityState.Modified))
   { 

    if(entry.Entity is IAuditable){
    //Start pseudo code.. 

    IAuditabletemp = entry.Entity as IAuditable;
    if(entry.State == EntityState.Added ){
       temp.SetInsertedOn(DateTime.Now);
       temp.SetInsertedBy(DataConstants.GetUserID());
    }
     if(entry.State == EntityState.Added || entry.State == EntityState.Modified){
       temp.SetUpdatedOn(DateTime.Now);
       temp.SetUpdatedBy(DataConstants.GetUserID());
     }
    }
} 

Onto the stuff that gives me nightmares. The modified T4 Template…

The first thing I did was go down to the block of using statements and add at statement to pull in our interface… not too repulsive..

using System;
using System.Data.Objects;
using System.Data.Objects.DataClasses;
using System.Data.EntityClient;
using System.ComponentModel;
using System.Xml.Serialization;
using System.Runtime.Serialization;
using System.Data;
using EntityFramework.Audit; 

 

If you scroll down in the T4 template to the “#region Contexts” this is where we added the “Code Trigger” to all of the constructors. Additionally, right beneath the constructors I actually also paste the code to handle the auditing…

#region Constructor

public <#=className#>() : base("name=<#=className#>", "<#=className#>")
{
   this.OnContextCreated();
   this.SavingChanges += new EventHandler(UpdateAuditInformation);
}

// SavingChanges event handler.
private void UpdateAuditInformation(object sender, EventArgs e)
{
   foreach (ObjectStateEntry entry in

Now for the promised nasty piece:

The only hard part of this process was to generate the “IAuditable“interface .The magical line, line 242 , is down in the #region Entities section. There is a section in the T4 template that iterates through each EntityType in your conceptual model and generates a class file to represent it. So I simplied added a section that calls a method to see if its IUpdateable, and if so it adds the interface.

public <#=entity.Abstract ? "abstract " : "" #>partial class <#=entity.Name#> : <#=entity.BaseType == null ? "EntityObject" : entity.BaseType.Name#> <#=ContainsIUpdateable(entity.Members.OfType<EdmProperty>().ToList()) ? ", IAuditable" : ""#>

If you look in the “Convenience methods” section in the T4 template you will see a method called ContainsIUpdateable that actually uses Linq to search through the properties of the conceptual entity for each one of the requried fields.

 

public bool ContainsIUpdateable(IEnumerable<EdmProperty> edmProperties){
//check to make sure it contains...

   if (
      (edmProperties.Where(x => x.Name == "InsertedOn").FirstOrDefault()) != null &&
      (edmProperties.Where(x => x.Name == "InsertedBy").FirstOrDefault()) != null &&
      (edmProperties.Where(x => x.Name == "UpdatedBy").FirstOrDefault()) != null &&
      (edmProperties.Where(x => x.Name == "UpdatedOn").FirstOrDefault()) != null
    ){
      return true;
    }
    return false;
}

The last and final step is the generation of the implementation of IAuditable inside the entity. This is done right before the Navigational Properties secion (line 349).

We insert a conditional code block that says, if ContainsIUpdateable generate this code:

<#if(ContainsIUpdateable(entity.Members.OfType<EdmProperty>().ToList())) {#>

#region IAuditable Members
public void SetInsertedOn(DateTime date)
{
    this.InsertedOn = date;
}

public void SetInsertedBy(string user)
{
    this.InsertedBy = user;
}

public void SetUpdatedOn(DateTime date)
{
   this.UpdatedOn = date;
}

public void SetUpdatedBy(string user)
{
   this.UpdatedBy = user;
}
#endregion
<#}
#>

And voila! Simple huh? T4 templates are actually very useful if you are using .NET 3.5. EF 4.0 is much more powerful, but unfortunately you need 2010 to develop in it. We actually also used the same T4 template to do some lazy loading. So if someone navigates to a link that was not loaded, it automagically loads it for them. This feature actually is coming out in EF 4, so its only useful for 3.5 or earlier.

About these ads
Tagged with:
Posted in C#, LinkedIn, Microsoft

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: