A Method to Handle Audit Fields in LINQ to SQL

Subsonic has this convention where it does this automatically for you, if the table had columns CreatedOn, CreatedBy, ModifiedOn, ModifiedBy. DLINQ doesn't really have anything like this built-in.  This isn't something one wishes to do manually, as to remember to update these columns every time you save something would be prone to forgetfulness as well as being a lot of extra typing.  So how might we wire up something similar to what SubSonic does?

I did a few searches on the internet, and while I found a couple of suggestions I didn't find an easy solution.  One suggestion was to extend the partial methods of the DataContext for Insert and Update.  This worked, but it means for every entity in your model you have to extend Insert and Update.  Fairly quickly you have several dozen of these things and you notice they're all doing the same thing.  Another suggestion involved extending SubmitChanges() in the DataContext, but it was rather complicated and relied upon reflecting upon the properties of the class to check if they had the right keywords in them set them appropriately.  I don't want to do reflection if I can otherwise avoid it.

What if we used our own convention?  Our tables use the same audit field names, just like with Subsonic.  When we pull the tables into the LINQ to SQL class builder, it creates all of our entity classes for us with properties for those fields.  Since the classes are partials, we can also extend them and claim they implement a common interface.

Examples are in C#, and you'll need to remember to create the interfaces and classes in the appropriate namespaces. 

First, we create an interface to define our contract. We create an IAuditable interface with properties for our audit fields.

    internal interface IAuditable
    {
        string CreatedBy { get; set; }
        DateTime CreatedOn { get; set; }
        string ModifiedBy { get; set; }
        DateTime ModifiedOn { get; set; }
    }

Now, we extend the classes created by LINQ to SQL, and note that they implement our interface.  This is relatively easy to do as they were partial classes. The properties should already be in the classes as they were created by LINQ to SQL based on our table definitions.  Here I just use a couple of example table classes, you'll need to remember to add the same syntax for each table as you add it to LINQ to SQL.

    public partial class Calendar : IAuditable
    {
    }

    public partial class Photo : IAuditable
    {
    }

    public partial class Content : IAuditable
    {
    }

    public partial class Discussion : IAuditable
    {
    }

Now, we override SubmitChanges in our DataContext.  I have it call a routine which updates the auditfields, passing in the list of all objects being managed by the datacontext.  It's nice that the DataContext method GetChangeSet() has lists for Inserts or Updates.  I'm using a utility class to perform the work.

        public override void SubmitChanges(System.Data.Linq.ConflictMode failureMode)
        {
            AuditUtility.ProcessInserts(base.GetChangeSet().Inserts);
            AuditUtility.ProcessUpdates(base.GetChangeSet().Updates);
            
            base.SubmitChanges(failureMode);
        }

And then finally our AuditUtility class.  Basically, it iterates through the list, checks to see if the object implements the IAuditable interface we created, and if so sets the auditfields.  This allows us to have tables which don't have audit fields, in which case they are simple ignored. I have a GetUserName() method inside that tries to identify the authenticated user whether it's web or local windows context, although this may need to be modified to handle the specifics of your app.

    internal static class AuditUtility
    {
        private static string GetUserName()
        {
            string userName = "";
            if (System.Web.HttpContext.Current != null)
                userName = System.Web.HttpContext.Current.User.Identity.Name;
            else
                userName = Environment.UserName;

            return userName;
        }

        private static void ProcessAuditFields(IList<System.Object> list, bool UpdateCreatedFields)
        {
            foreach (var item in list)
            {
                IAuditable entity = item as IAuditable;
                if (entity != null)
                {
                    if (UpdateCreatedFields)
                    {
                        entity.CreatedBy = GetUserName();
                        entity.CreatedOn = DateTime.Now;
                    }
                
                    entity.ModifiedBy = GetUserName();
                    entity.ModifiedOn = DateTime.Now;
                }
            }
        }

        internal static void ProcessInserts(IList<System.Object> list)
        {
            ProcessAuditFields(list, true);
        }

        internal static void ProcessUpdates(IList<System.Object> list)
        {
            ProcessAuditFields(list, false);
        }

    }

I hope that this technique can be of some use to others, and would value any feedback on how to make it work better.

 

Published Saturday, February 23, 2008 7:27 PM by sodablue
Filed under: , ,

Comments

# re: A Method to Handle Audit Fields in LINQ to SQL

Sunday, February 24, 2008 2:33 PM by Josh Schwartzberg

I would suggest using dependency injection to pass a class that provides the means for retrieving the current user to make this audit implementation more usable from different environments.

# re: A Method to Handle Audit Fields in LINQ to SQL

Monday, February 25, 2008 1:01 PM by sodablue

That's a very good idea.  After posting this, I started playing with the Unity CTP this weekend so I'll take a look at that.

# re: A Method to Handle Audit Fields in LINQ to SQL

Monday, February 25, 2008 4:06 PM by Michael Third

You probably also want to determine now only once, otherwise you end up with Created and Modified being slightly different.

# re: A Method to Handle Audit Fields in LINQ to SQL

Thursday, February 28, 2008 5:04 AM by Curufinwe

The solution with reflection may look a bit complicated but it is "write once and forget" solution (it will handle all tables that follow naming convention for audit fields). And most of the reflection work is done at program start (in static constructor) so overhead is minimal.  

# re: A Method to Handle Audit Fields in LINQ to SQL

Friday, May 09, 2008 2:20 PM by Matt Hidinger

Good looking solution. I recently had to add an Audit Trail to my project, and came up with a "similar" solution. I'd be interested in your thoughts on my approach.

The article can be found here:

blog.matthidinger.com/.../LINQToSQLAuditTrail.aspx

# re: A Method to Handle Audit Fields in LINQ to SQL

Saturday, June 14, 2008 4:26 AM by Kent

Great. Is there a vb version of this?

# re: A Method to Handle Audit Fields in LINQ to SQL

Saturday, June 14, 2008 5:59 AM by George

You forgot to implement audit trail on Deletes

# re: A Method to Handle Audit Fields in LINQ to SQL

Sunday, October 25, 2009 4:13 PM by Brady Kelly

@George, a deleted record has no audit fields!

# re: A Method to Handle Audit Fields in LINQ to SQL

Friday, June 04, 2010 5:19 AM by yashwanthb

nice article . I have implemented same process in my projects ..

# Generic/ Inherited Metadata Possible? | Info

Monday, December 06, 2010 3:41 PM by Generic/ Inherited Metadata Possible? | Info

Pingback from  Generic/ Inherited Metadata Possible? | Info

# re: A Method to Handle Audit Fields in LINQ to SQL

Tuesday, January 11, 2011 9:57 AM by padkavi

I am trying to use this approach for my project. But I am getting errors on the lines

base.GetChangeSet().Inserts);

base.GetChangeSet().Updates);

base.SubmitChanges(failureMode);

Its saying System.Bbject does not contain definition for getChangeSet() and SubmitChanges()...

How do I fix this compilation error.. Any help would be appreciated.

Thanks

# Linq auditing | Jewelryfundrai

Monday, January 23, 2012 7:41 PM by Linq auditing | Jewelryfundrai

Pingback from  Linq auditing | Jewelryfundrai

Leave a Comment

(required) 
(required) 
(optional)
(required)