A couple months ago I wrote a basic set of extension methods to handle automatic auditing in LINQ to SQL. Well I have received a large number of emails regarding this particular project so I have decided to focus on cleaning up my v2 API and releasing it on CodePlex. There was a lot of room for improvement from version 1 and today I am going to post the all new LINQ Audit Trail code. This new version is significantly enhanced in the previous version.

Objective

Automatic auditing of all inserts/updates/deletes for any table in your database with a single line of code, including:

  • What table was modified?
  • What fields changed?
  • Who made the change?
  • When did it occur?


Usage

Simply define your audit definitions at any time before calling SubmitChanges();

this.Products.Audit();
this.Categories.Audit();
this.Orders.Audit().AuditAssociation(o => o.Order_Details);
this.Contacts.Audit().AuditAssociation(c => c.Addresses).AuditAssociation(c => c.PhoneNumbers);

Updates and Download

Please see the CodePlex project for updates and new releases. DoddleAudit


New Features and Fixes in V2

  • A significantly refined API for defining which tables to audit, including automatic primary key lookups.
  • Inserted records will have their primary keys correctly stored in the audit table now.
  • Built-in support for auditing across relationships.
    • E.g., assume you want to audit a Contacts table which has a 1-to-many relationship to the Addresses table, thus allowing any number of Addresses to each Contact. Well ideally you want to show these Address audits on your ContactDetails.aspx, which is exactly what you can see in the screenshot below. 
      image

  • Ability to define custom “audit property resolvers” to override the default auditing mechanism for properties that you specify.
    • For example, notice in the above screenshot that “Address Type” has a value of “1” since that is how the data is stored in the table. This number does little good for the end user, so in V2 it is now possible to customize specific properties as needed. I will explain more details about this soon. Below I am overriding the default auditing of the Product.CategoryID property by querying the category by looking up the CategoryID, returning the Category.CategoryName, and renaming the audit field to “Category” since “CategoryID” is no longer accurate.
    public class ProductAuditResolver : AuditPropertyResolver
    {
        protected override void CustomizeProperties()
        {
            CustomizeProperty(p => p.CategoryID, categoryId => GetCategoryByID(cid).CategoryName, "Category");
        }
    }	
       


    Instructions

    1. Download the latest release from CodePlex, the source code and compiled DLL are available, and add reference to Doddle.Linq.Audit.dll in your project that contains your LINQ to SQL DBML
    2. At a minimum you will need to add 2 tables to your database (and your DBML) to store the audit records. Please see the schema below and add these tables to your database.
      • NOTE: Keep in mind this database schema is entirely customizable. I chose to use two tables to store all of my audits, but you could very easily change this logic to use a separate table for each entity or whatever storage schema you choose.  
        image
    3. Open your DBML and click in the designer surface. In the property pane you will need to change the Base Class property of your generated DataContext to be Doddle.Linq.Audit.LinqToSql.AuditableDataContext 
      image

    4. Lastly you will need to create a partial DataContext class to wire up the auditing infrastructure to match your database schema. Add a new Class file to your project and insert the following code. Customize if necessary to match your auditing schema.
      public partial class NorthwindEntitiesDataContext
      {
          protected override void InsertAuditRecordToDatabase(EntityAuditRecord record)
          {
              AuditRecord audit = new AuditRecord();
              audit.Action = (byte)record.Action;
              audit.AuditDate = DateTime.Now;
              audit.AssociationTable = record.AssociationTable;
              audit.AssociationTableKey = record.AssociationTableKey;
              audit.EntityTable = record.EntityTable;
              audit.EntityTableKey = record.EntityTableKey;
      
              audit.UserName = HttpContext.Current.User.Identity.Name;
      
              foreach (ModifiedEntityProperty av in record.ModifiedProperties)
              {
                  AuditRecordModifiedField field = new AuditRecordModifiedField();
                  field.MemberName = av.MemberName;
                  field.OldValue = av.OldValue;
                  field.NewValue = av.NewValue;
      
                  audit.AuditRecordModifiedFields.Add(field);
              }
      
              this.AuditRecords.InsertOnSubmit(audit);
          }
      
          protected override void DefaultAuditDefinitions()
          {
              this.Products.Audit();
              this.Categories.Audit().AuditAssociation(c => c.Products);
          }
      }

    Known Issues

    1. I have not finished the support for the Entity Framework implementation of the auditing yet. If anyone more experienced with EF is out there downloading this source please let me know if you want to help complete it.

    Hopefully that is enough to get anyone started with the code. I am certainly looking for feedback for any suggestions, problems, or improvements. After some more unit testing and perhaps some slight refinements to the API I will remove the “beta” moniker.

    Technorati Tags: ,,

  • Comments

    # re: LINQ Audit Trail v2 - DoddleAudit

    Gravatar
    Jan
    3/3/2009 7:05 AM
    Works great in our small Project. We needed to modify it slightly, so that string-Pks can be used, but that was not a big deal.
    Very helpful, this DoddleAudit

    # re: LINQ Audit Trail v2 - DoddleAudit

    Gravatar
    Dave
    3/4/2009 12:54 PM
    This looks like it will be very helpful for an upcoming project. Nice work.

    # re: LINQ Audit Trail v2 - DoddleAudit

    Gravatar
    CarlH
    3/5/2009 7:48 AM
    Hey, great utility!

    But please change "
    public static AuditDefinition<TEntity> Audit<TEntity>(this Table<TEntity> table) where TEntity : class;"

    to take ITable as first argument, so that you can do something like this:

    foreach (var type in System.Reflection.Assembly.GetExecutingAssembly().GetTypes())
    {
    this.GetTable(type).Audit();

    }

    i have about 40 tables in my DB and don't want to manually set up audits for them all..

    # re: LINQ Audit Trail v2 - DoddleAudit

    Gravatar
    Angely
    3/20/2009 8:20 AM
    How do I audit many-to-many relations.

    Lets say I have three tables - Users, Roles and UserRoles, where UserRoles is a junction table.

    It seems to me this is not possible, am I right?

    # re: LINQ Audit Trail v2 - DoddleAudit

    Gravatar
    Terry Walker
    3/25/2009 4:52 AM
    I was just thinking about LINQ Audit Trail v2 - DoddleAudit and you’ve really helped out. Thanks!

    # re: LINQ Audit Trail v2 - DoddleAudit

    Gravatar
    tec-goblin
    3/30/2009 7:14 AM
    Lovely! Exactly what we were searching.
    Be prepared for some questions when we finally use it :).

    # re: LINQ Audit Trail v2 - DoddleAudit

    Gravatar
    Brian
    4/2/2009 8:07 AM
    Is it possible to have multiple levels of audits?

    Let's say I have order, order_detail, and order_detail_Charges and I want to audit all three tables.

    I am able to show only the two levels (as below):

    this.GetDC().Order.Audit(c => c.Order_id).AuditAssociation<Order_Detail>(x => x.Order_detail_id, x => x.Order_id);

    How to audit the order_detail_Charges table under the order and order_detail?


    Second question:

    Is it possible to handle a table which can have multiple parents which would be defined during the same 'save event'?

    # re: LINQ Audit Trail v2 - DoddleAudit

    Gravatar
    Shalu
    5/12/2009 2:07 PM
    Is there something similar in vb.net. I tried to convert C# to vb.net and am lost.

    # re: LINQ Audit Trail v2 - DoddleAudit

    Gravatar
    Diego
    5/19/2009 9:58 AM
    When you think a next generation version that includes auditing tables with primary key consisting of several columns?????
    i need it urgent!!!!

    # re: LINQ Audit Trail v2 - DoddleAudit

    Gravatar
    Texas Holdem Tournaments
    7/14/2009 3:25 AM
    I have also included the sample Northwind Project so you can play with the demo yourself."
    Can u please tell me where did u have put "Northwind project"?

    # re: LINQ Audit Trail v2 - DoddleAudit

    Gravatar
    Asger Jensen
    8/7/2009 9:01 AM
    Hi; thank you for your great work.

    I have a small problem when it comes to AuditAssociation;

    I have an Order table and an OrderItem table.

    The OrderItem table has an OrderId foreign key to show which order it belongs to.

    When I add

    this.Orders.Audit().AuditAssociation(o => o.OrderItems);

    it compiles fine, but at runtime it throws this exception:

    Expression of type 'System.Int32' cannot be used for return type 'System.Nullable`1[System.Int32]

    ...
    at Doddle.Linq.Audit.AuditExtensions.GetEntityPropertySelector[TEntity,TProp](IAuditableContext context, String propertyName)\r\n at Doddle.Linq.Audit.AuditDefinition`1.AuditAssociation[TAssociation](Expression`1 relatedEntity)\r\n at Renova.Models.RenovaDataContext.DefaultAuditDefinitions()


    Can you clue me what to do?

    Thanks in advance.

    # re: LINQ Audit Trail v2 - DoddleAudit

    Gravatar
    Andrej Kovacik
    8/26/2009 4:41 PM
    I just owe you big thank you for your great work. I wanted to implemet something similar but I was trying to save objects as XML ... took forever and it didn't work. This is simple, clean and most of all universal (up to LINQ to SQL limits).

    Keep up the good work and good luck with future versions.

    A.

    # re: LINQ Audit Trail v2 - DoddleAudit

    Gravatar
    Andrej Kovacik
    9/3/2009 3:38 PM
    So now I've tested your great classes a little bit more with our database and I ran into few issues

    All values set during OnValidate() are not recorded correctly under [New Value] (they keep the values from before OnValidate(). This makes some of the fields in audit not correct when compared to data saved to database.

    I understand that OnValidate might not be the best place to change values but so far it is the only event I can use and I need to reset some fields base on business rules just before I post them to DB and I need this to be done in one place (OnPropertyChanged is not always suitable as I need to compare multiple properties at once)

    Any ideas....?

    # re: LINQ Audit Trail v2 - DoddleAudit

    Gravatar
    Raman
    9/23/2009 4:36 AM
    Awesome

    # re: LINQ Audit Trail v2 - DoddleAudit

    Gravatar
    VUONG
    10/8/2009 1:17 PM

    So now I've tested your great classes a little bit more with our database and I ran into few issues

    All values set during OnValidate() are not recorded correctly under [New Value] (they keep the values from before OnValidate(). This makes some of the fields in audit not correct when compared to data saved to database.

    I understand that OnValidate might not be the best place to change values but so far it is the only event I can use and I need to reset some fields base on business rules just before I post them to DB and I need this to be done in one place (OnPropertyChanged is not always suitable as I need to compare multiple properties at once)

    # re: LINQ Audit Trail v2 - DoddleAudit

    Gravatar
    Cesar
    11/24/2009 2:25 PM
    Hello...

    I am having the same issue as Asger.

    I have a Members table and a MembersAddress Table

    It compiles fine.

    When I try updating a MemberAddress entry I get the same error as Asger.

    this.Members.Audit();
    this.Members.Audit().AuditAssociation(m => m.MembersAddress);

    # re: LINQ Audit Trail v2 - DoddleAudit

    Gravatar
    Saint Ari
    12/16/2009 6:24 AM
    I am using VS 2010 Beta 2 and I seem to have a problem with it.

    Its the call to DataBinder.Eval that refers System.Web.UI ....

    You seem to have a to-do there to remove the dependency ...

    I tried to recompile it using Framework 4.0 and for some reason, it keeps on reverting to the client profile.

    Could you shed some light on this please?

    Thanks!!!

    # re: LINQ Audit Trail v2 - DoddleAudit

    Gravatar
    win money in the casino
    12/28/2009 5:36 AM
    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.

    # re: LINQ Audit Trail v2 - DoddleAudit

    Gravatar
    steve potter
    1/8/2010 11:13 AM
    Great work!

    Bug: if SubmitChanges() is called more than once, the audit records get duplicated. There are reasons for calling SubmitChanges() multiple times, so it's good to support it. This bug can be easily fixed by adding _queuedRecords.Clear(); under the foreach loop in AuditableDataContext.SubmitChanges.

    Also a few suggestions:
    - Consider supporting separate audit tables for each entity being audited. For example, a Customer table has a cooresponding Customer_Audit table. Support for generating these tables also would be killer.
    - Also consider documenting how to use an xml column to store each audit record field. This allows for a single audit table (no need for AuditRecordFields).

    Again, great work. Keep it up!

    # re: LINQ Audit Trail v2 - DoddleAudit

    Gravatar
    Manish
    1/11/2010 3:29 AM
    Great work, I have not tried it yet bu tlook great!
    The one thing i read was no support for guids at the moment and composites PK, is this something that you are planning?

    I have many tables some have PK as int and others as guids, if you are not planning on updating soon can you give me a pointer on where i should start to change your code, I am guessing

    private static Audit CreateAudit<TEntity>(string title, Table<TEntity> table, int key) where TEntity : class

    and create a new method
    private static Audit CreateAudit<TEntity>(string title, Table<TEntity> table, System.Guid key) where TEntity : class

    and also update
    foreach (TSubEntity item in inserts)
    {
    // Get the Primary Key for our table by Invoking the tableKeySelector delegate on the current TSubEntity item
    int key = tableKeySelector.Invoke(item);

    and lastly

    foreach (TSubEntity item in updates)
    {
    int key = tableKey.Invoke(item);

    am I heading in the right direction?
    Comments have been closed on this topic.