WCF RIA Services and Audit – Historical changes

Note: The examples in this blog post is based on the WCF RIA Services PDC Beta and VS2010 Beta Preview, and changes to RIA Services can be done until it hits RTM.

This blog post is about audit historical changes of entities. Some users wants to see a historical overview of changes made to a entity and there are several solutions for auditing. If we use Microsoft SQL Server 2008 we can for example use the Change Tracking and Change Data Capture feature (Thanks to Colin Blair who gave me the pointers to the SQL 2008 features). If we use Entity Framework we can enable Auditing on the ObjectContext and if we use nHibernate we can also handle audit by using IInterceptor or by using Event listeners. But assume we don’t have Entity Framework or nHibernate etc and we want a way to handle Audit. WCF RIA Services’s DomainService uses a ChangeSet where it adds the changes passed from the DomainContext to the DomainSerivce. By using the ChangeSet we can gather information that can be used for auditing.

public sealed class ChangeSet
{
   public ChangeSet(IEnumerable<ChangeSetEntry> changeSetEntries);

   public ReadOnlyCollection<ChangeSetEntry> ChangeSetEntries { get; }

   public bool HasError { get; }

   public void Associate<TEntity, TStoreEntity>(TEntity clientEntity, TStoreEntity storeEntity, Action<TEntity, TStoreEntity> storeToClientTransform)
            where TEntity : class
            where TStoreEntity : class;

   public IEnumerable GetAssociatedChanges<TEntity, TResult>(TEntity entity, Expression<Func<TEntity, TResult>> expression);

   public IEnumerable GetAssociatedChanges<TEntity, TResult>(TEntity entity, Expression<Func<TEntity, TResult>> expression, ChangeOperation operationType);

   public IEnumerable<TEntity> GetAssociatedEntities<TEntity, TStoreEntity>(TStoreEntity storeEntity) where TEntity : class;

   public ChangeOperation GetChangeOperation(object entity);

   public TEntity GetOriginal<TEntity>(TEntity clientEntity) where TEntity : class;

   public void Replace<TEntity>(TEntity clientEntity, TEntity returnedEntity) where TEntity : class;
}

 

By iterating through the ChangeSet’s ChangeSetEntries we can get the changes made to entities, for example if it’s Created, Update or Deleted etc. The ChangeSetEntry holds the OriginalEntity and the current Entity when an update is made. But there is no information about properties that have been changed between the Original and Current entity. So I have created an Extension method to gather the information needed for auditing and created a new set of classes:

public class EntityChangeInfo
{
    public string EntityName { get; internal set; }

    public object Entity { get; internal set; }

    public DomainOperation Operation { get; internal set; }

    public IEnumerable<PropertyChangeInfo> Changes { get; internal set; }
}


public class PropertyChangeInfo
{
     public string PropertyName { get; internal set; }

     public object OriginalValue { get; internal set; }

     public object CurrentValue { get; internal set; }
}


What mostly is needed when creating a historical record are the following information:


Operation (Delete, Insert, Updated).

Entity, which entity was deleted, created or updated.

Property Changes where the original and current value is needed so the user can see what the previous value was before the changes took place.

Date and time when the changes was made.

Updated by and created by, so the user know who created and updated the value.


The EntityChangeInfo will contain the name of the Entity, the current Entity instance, DomainOperation (Delete, Insert, Update) and information about the properties that was changed between the original entity and the current entity. The PropertyChangeInfo class will hold information about the Property that was changed and its original and current value. I needed a way to compare two objects with each other to find the properties that have been changed, I decided to only compare the value of a property where the type implements the ICompareable interface, such as (Int, Double, String etc). The following is an extension method of the ChangeSet type where a collection of EntityChangeInfo is created:

public static class ChangeSetHelper
{
    public static IEnumerable<EntityChangeInfo> ToEntityChangeInfo(this ChangeSet changeSet)
    {
        if (changeSet == null)
            throw new ArgumentNullException("changeSet");

        var entitiesChanges = new List<EntityChangeInfo>();

        foreach (var changeSetEntry in changeSet.ChangeSetEntries)
        {
            var entityChangeInfo = new EntityChangeInfo()
                                   {
                                       Operation = changeSetEntry.Operation,
                                       EntityName = changeSetEntry.Entity.GetType().Name,
                                       Entity = changeSetEntry.Entity
                                   };

             if (changeSetEntry.Operation == DomainOperation.Update)
                 entityChangeInfo.Changes = GetPropertiesChangeInfo(
                                                    changeSetEntry.Entity,
                                                    changeSetEntry.OriginalEntity);

             entitiesChanges.Add(entityChangeInfo);
         }

         return entitiesChanges;
     }

     private static IEnumerable<PropertyChangeInfo> GetPropertiesChangeInfo(object x, object y) 
     {
         PropertyInfo[] properties = x.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public); 
            
         var propertyChanges = new List<PropertyChangeInfo>();

         foreach (PropertyInfo property in properties) 
         { 
             IComparable valueOfX = property.GetValue(x, null) as IComparable;

             if (valueOfX == null) 
                 continue;

             object valueOfY = property.GetValue(y, null);

             compareValue = valueOfX.CompareTo(valueOfY);

             if (compareValue != 0)
             {
                 propertyChanges.Add(new PropertyChangeInfo()
                                     {
                                         CurrentValue = valueOfX,
                                         OriginalValue = valueOfY,
                                         PropertyName = property.Name
                                     });

             }
         }

         return propertyChanges; 
    } 
}

 

One place where the ToEntityChangeInfo extension method can be used is in the DomainService PersistChangeSet method (The method is called when all changes should be persisted):

protected override bool PersistChangeSet(ChangeSet changeSet)
{
   var hasError = false;

    using (var tran = new TransactionScope())
    {
        _audit.LogChanges(changeSet.ToEntityChangeInfo());

         hasError = !base.PersistChangeSet(changeSet);

         if (!hasError)
             tran.Complete();
     }

     return hasError;
}


Note: The TransactionScope is used here to rollback the Audit information if the PersistChangeSet returns a false value.

The _audit.LogChanges is only a simple class I have added to write the changes to the Debug class, here is the code for the Audit class:

 

public class Audit
{
    public void LogChanges(IEnumerable<EntityChangeInfo> changes)
    {
        foreach (var change in changes)
        {
            if (IsEntityDeletedOrInserted(change))
                LogDeleteAdnInsertOperationWithDebug(change);
            else if (IsEntityUpdatedAndHaveChanges(change))
                LogChangesWithDebug(change);
        }
    }

    private static void LogChangesWithDebug(EntityChangeInfo entityChangeInfo)
    {
       foreach (var propertyChange in entityChangeInfo.Changes)
       {
           Debug.WriteLine(
                 string.Format("{0} – ID: {1}, {2}, Property: {3}, From value: '{4}', To value: '{5}', {6}, By: {7}",
                         entityChangeInfo.Operation,
                         entityChangeInfo.Entity,
                         entityChangeInfo.EntityName,
                         propertyChange.PropertyName,
                         propertyChange.OriginalValue,
                         propertyChange.CurrentValue,
                         DateTime.Now,
                         Thread.CurrentPrincipal.Identity.Name));
        }
    }

    private static bool IsEntityDeletedOrInserted(EntityChangeInfo entityChangeInfo)
    {
        return entityChangeInfo.Operation == DomainOperation.Delete || 
                                                          entityChangeInfo.Operation == DomainOperation.Insert;
    }

    private static bool IsEntityUpdatedAndHaveChanges(EntityChangeInfo entityChangeInfo)
    {
        return entityChangeInfo.Operation == DomainOperation.Update && 
                                                          entityChangeInfo.Changes != null;
    }

    private static void LogDeleteAdnInsertOperationWithDebug(EntityChangeInfo entityChangeInfo)
    {
        Debug.WriteLine(string.Format("{0} – ID: {1}, {2}, {3}, By: {4}",
                                 entityChangeInfo.Operation,
                                 entityChangeInfo.Entity,
                                 entityChangeInfo.EntityName,
                                 DateTime.Now,
                                 Thread.CurrentPrincipal.Identity.Name));
    }
}

 

If the client-side code looks like this:

 

CustomerDomainContext _customerDomainContext = new CustomerDomainContext();

public MainPage()
{
     InitializeComponent();

     _customerDomainContext.Load<Customer>(
                   _customerDomainContext.GetCustomersQuery(),
                   LoadCustomerCompleted,
                   null);
}

private void LoadCustomerCompleted(LoadOperation loadOperation)
{
    _customerDomainContext.Customers.Remove(_customerDomainContext.Customers.Last());

    var newCustomer = new Customer()
             { ID= 3,  Name = "New John Doe", CreatedBy = "test", UpdatedBy = "test" };

     newCustomer.Orders.Add(
             new Order() { ID = 1, CustomerID = 2, Name = "Test" });

    _customerDomainContext.Customers.Add(newCustomer);


     var updateCustomer = _customerDomainContext.Customers.First();

     updateCustomer.Name = "Updated John Doe";

     updateCustomer.Orders.Remove(updateCustomer.Orders.Last());

     updateCustomer.Orders.Add(
             new Order() { ID = 2, CustomerID = 1, Name = "Test" });

     updateCustomer.Orders.First().Name = "Update Name";

    _customerDomainContext.SubmitChanges();
}

 

When DomainSerivice’s PersistChangeSet make a call to the Audit’s LogChanges method the following information will bee displayed in the VS output window:

Insert – ID: 3, Customer, 2010-02-27 14:31:46, By: Fredrik
Insert – ID: 1, Order, 2010-02-27 14:31:46, By: Fredrik
Insert – ID: 2, Order, 2010-02-27 14:31:46, By: Fredrik
Update – ID: 1, Customer, Property: Name, From value: 'John Doe', To value: 'Updated John Doe', 2010-02-27 14:31:46, By: Fredrik
Update – ID: 10, Order, Property: Name, From value: 'Test', To value: 'Update Name', 2010-02-27 14:31:46, By: Fredrik
Delete – ID: 2, Customer, 2010-02-27 14:31:46, By: Fredrik

 

If you want to know when I publish a blog post, you can follow me on twitter: http://www.twitter.com/fredrikn

2 Comments

Comments have been disabled for this content.