Skip to main content

Implementing/Automating audit logs in Telerik Data Access

Audit logs can be tedious task if done manually, also developer might miss to update audit log implementation on certain level. The codes would be repeated on all places if not centralized.

There are many approach available to maintain change history of model/table. Like having single history table and manage all changes of all models in same table. We may maintain in same table with some flags and JSON data for change list.

We will look for maintaining history table based on each required data models with minimum effort and performance. To reduce code, I am going to use T4 to generate history models automatically based on original model. Also we are going to take care of Artificial type values.

Step 1 - Create a custom attribute to mark model that history need to be maintained.


   /// <summary>  
   /// Attribute to maintain history table  
   /// </summary>  
   [AttributeUsage(AttributeTargets.Class)]  
   public class ManageHistoryAttribute  
     : Attribute  
   {  
     /// <summary>  
     /// Initializes a new instance of the <see cref="ManageHistoryAttribute"/> class.  
     /// </summary>  
     public ManageHistoryAttribute()  
     {  
     }  
     /// <summary>  
     /// Initializes a new instance of the <see cref="ManageHistoryAttribute"/> class.  
     /// </summary>  
     /// <param name="historyModelName">Name of the history model.</param>  
     public ManageHistoryAttribute(string historyModelName)  
     {  
       HistoryModelName = historyModelName;  
     }  
     /// <summary>  
     /// Gets or sets the name of the history model.  
     /// </summary>  
     /// <value>  
     /// The name of the history model.  
     /// </value>  
     public string HistoryModelName { get; private set; }  
   }  

HistoryModelName is for marking if we want to have specific name that we want to give to history table, otherwise constructor is not required.

Now, this attribute can be decorated in desired data models.
Ex:
 [ManageHistory]  
 public class TestModel : IPrimaryKey
 {  
     public int Id { get; set; }  
     public string Name { get; set; }  
 }  

An interface to have common Id column which acts as primary key for all models. Can be generic as well.

   /// <summary>  
   /// Primary key implementation  
   /// </summary>  
   public interface IPrimaryKey  
   {  
     int Id { get; set; }  
   }  

Step 2 - Creating interface for history models.
The interfaces are really nice way to group anything which are sharing common specification. The benefits may be unknown on design time but there would be huge possibility of surprise elements in future development of project.

This is the interface that would be used under each history models and we will look for this interface on OpenAccess context events.

   /// <summary>  
   /// Interface of Audit/History model  
   /// </summary>  
   public interface IModelHistory  
     : IPrimaryKey  
   {  
     /// <summary>  
     /// Gets or sets the history model identifier.  
     /// </summary>  
     /// <value>  
     /// The history model identifier.  
     /// </value>  
     int HistoryModelId { get; set; }  

     /// <summary>  
     /// Gets or sets the user id.  
     /// </summary>  
     /// <value>The user id.</value>  
     int? UserId { get; set; }  

     /// <summary>  
     /// Gets or sets the created at.  
     /// </summary>  
     /// <value>  
     /// The created at.  
     /// </value>  
     DateTime CreatedAt { get; set; }  

     /// <summary>  
     /// Gets or sets the operation.  
     /// </summary>  
     /// <value>  
     /// The operation.  
     /// </value>  
     DbOperation Operation { get; set; }  
   }  

DbOperation is enum for knowing type of operation is done on history table.

   /// <summary>  
   /// Database operation  
   /// </summary>  
   public enum DbOperation  
   {  
     Insert,  
     Delete,  
     Update  
   }  

Step 3 - Custom attribute for history models to know original source item.

This attribute would be used on history models to know source item.

   /// <summary>  
   /// Attribute for judging source of history model.  
   /// </summary>  
   [AttributeUsage(AttributeTargets.Class)]  
   public class ManageHistorySourceAttribute  
      : Attribute  
   {  
     /// <summary>  
     /// Gets or sets the source.  
     /// </summary>  
     /// <value>  
     /// The source.  
     /// </value>  
     public Type Source { get; private set; }  
     /// <summary>  
     /// Initializes a new instance of the <see cref="ManageHistorySourceAttribute"/> class.  
     /// </summary>  
     /// <param name="source">The source of history model.</param>  
     public ManageHistorySourceAttribute(Type source)  
     {  
       Source = source;  
     }  
   }  

Step 4 - History model generation.
Basic things are set up. Now, we need to generate history models from original model. Let's use T4 to generate same.

External resource
I have took help of two libraries to read project files and to create multiple files.
MultiOutput.ttinclude (https://github.com/subsonic/SubSonic-3.0-Templates/blob/master/ActiveRecord/MultiOutput.ttinclude) : This is to generate multiple files through single T4 file.
VisualStudioAutomationHelper.ttinclude (https://github.com/PombeirP/T4Factories/blob/master/T4Factories.Testbed/CodeTemplates/VisualStudioAutomationHelper.ttinclude) : This will help us in reading files from different project.
NOTE: Please change path of these files based on path in your project.

 <#@ template debug="true" hostSpecific="true" #>  
 <#@ CleanupBehavior processor="T4VSHost" CleanupAfterProcessingtemplate="true" #>  
 <#@ assembly name="System.ComponentModel.DataAnnotations.dll" #>  
 <#@ import namespace="System" #>  
 <#@ import namespace="System.Diagnostics" #>  
 <#@ import namespace="System.Collections.Generic" #>  
 <#@ import namespace="System.Text.RegularExpressions" #>  
 <#@ import namespace="System.ComponentModel.DataAnnotations" #>  
 <#@ import namespace="EnvDTE" #>  
 <#@ import namespace="EnvDTE80" #>  
 <#@ include file="EF.Utility.CS.ttinclude"#>  
 <#@ include file = "MultiOutput.tt" #>  
 <#@ include file="VisualStudioAutomationHelper.ttinclude" #>  
 <#    
   // Formatting helper for code  
   CodeGenerationTools code = new CodeGenerationTools(this);  
   var namespaceName = code.VsNamespaceSuggestion();  
   // get a reference to the project of this t4 template  
   var project = VisualStudioHelper.CurrentProject;  
      var atributeFullName="MyProject.Model.CustomAttribute.ManageHistoryAttribute";  
   // TODO: Add namespace if having any other additional enum.  
   var whiteListPropertyTypes = new List<string>()  
   {  
     "MyProject.Model.Enumeration.DbOperation",  
   };  
   //TODO: Modify LINQ where your model resides.  
   // get all class items from the code model  
   var allClasses = (VisualStudioHelper.CodeModel.GetAllCodeElementsOfType(project.CodeModel.CodeElements, EnvDTE.vsCMElement.vsCMElementClass, false)  
                                    .Where(clas=>clas.FullName.StartsWith("MyProject.Model.DomainModel") &&  
                                    !clas.FullName.EndsWith("MetadataSource") &&  
                                    (clas as CodeClass).Attributes.OfType<CodeAttribute>().Any(attrib=>attrib.FullName == atributeFullName)  
                               ).ToList());  
   // iterate all classes  
   foreach(CodeClass codeClass in allClasses)  
   {  
     var historyModel = codeClass.Attributes.OfType<CodeAttribute>()  
       .FirstOrDefault(attrib=>attrib.FullName == atributeFullName);  
     var className = String.IsNullOrEmpty(historyModel.Value) ? "History" + codeClass.Name : historyModel.Value;  
     string fileName = className + ".cs";  
     var historyMapping = new StringBuilder();  
     var hasKeyInterface = VisualStudioHelper.CodeModel.GetAllImplementedInterfaces(codeClass)  
       .Any(interf => interf.Name == "IPrimaryKey");  
     var interfaceName = hasKeyInterface ? "IPrimaryKey," : String.Empty;  
 #>  
 //------------------------------------------------------------------------------  
      // <auto-generated>  
      //   This code was generated from a template and will be overwritten as soon   
      //       as the template is executed.  
      //  
      //   Changes to this file may cause incorrect behavior and will be lost if  
      //   the code is regenerated.  
      // </auto-generated>  
      //------------------------------------------------------------------------------  
 using System;  
 using System.ComponentModel.DataAnnotations;  
 using MyProject.Model.CustomAttribute;  
 using MyProject.Model.Enumeration;  
 using MyProject.Common.ExtensionMethods;  
 using MyProject.Model.ModalConstraint;  
 namespace <#=      namespaceName #>  
 {  
   /// <summary>  
   /// History model of <see cref="<#=      codeClass.FullName#>"/>.  
   /// </summary>  
   [ManageHistorySource(typeof(<#=      codeClass.FullName#>))]  
   public class <#=      className #>  
     : IPrimaryKey, IModelHistory  
   {  
 <#  
     // iterate all properties  
     var allProperties = VisualStudioHelper.CodeModel.GetAllCodeElementsOfType(codeClass.Members, EnvDTE.vsCMElement.vsCMElementProperty, true)  
       .OfType<CodeProperty2>();  
     if(hasKeyInterface){  
       #>/// <summary>  
       /// Gets or sets the Id.  
       /// </summary>  
       public int Id { get; set; }  
                <#  
       historyMapping.AppendFormat("\t\t\thistoryModel.HistoryModelId = model.Id;{0}",Environment.NewLine);  
     }  
     foreach(var property in allProperties  
       .Where(prop=>prop.ReadWrite == vsCMPropertyKind.vsCMPropertyKindReadWrite && prop.Name != "Id" &&  
       (IsPrimitive(prop.Type) || whiteListPropertyTypes.Contains(prop.Type.AsFullName))))  
     {  
       var propType = GetFriendlyPropertyTypeName(property);  
                #>/// <summary>  
       /// Gets or sets the <#=        property.Name #>.  
       /// </summary>  
                <#  
       foreach(var attribute in GetAttributes(property))  
       {  
           #>[<#=          attribute.Name #><#=          attribute.Value.Length>0?string.Format("({0})",attribute.Value):attribute.Value #>]  
 <#      }#>  
           public <#=        propType #> <#=        property.Name #> { get; set; }  
                     <#  
       historyMapping.AppendFormat("\t\t\thistoryModel.{0} = model.{0}{2};{1}",property.Name,Environment.NewLine,  
             ((property.Type.AsString=="System.DateTime" || property.Type.AsString=="System.DateTime?")?".ToUniversalTime()":String.Empty));  
     }  
 #>  
     #region " IModelHistory implementation "  
     /// <summary>  
     /// Gets or sets the history model identifier.  
     /// </summary>  
     /// <value>  
     /// The history model identifier.  
     /// </value>  
     public int HistoryModelId { get; set; }  
     /// <summary>  
     /// Gets or sets the user id.  
     /// </summary>  
     /// <value>The user id.</value>  
     public int? UserId { get; set; }  
     /// <summary>  
     /// Gets or sets the created at.  
     /// </summary>  
     /// <value>  
     /// The created at.  
     /// </value>  
     public DateTime CreatedAt { get; set; }  
     /// <summary>  
     /// Gets or sets the operation.  
     /// </summary>  
     /// <value>  
     /// The operation.  
     /// </value>  
     public DbOperation Operation { get; set; }  
     #endregion " IModelHistory implementation "  
     /// <summary>  
     /// Performs an explicit conversion from <see cref="<#=      codeClass.FullName#>"/> to <see cref="<#=      className#>"/>.  
     /// </summary>  
     /// <param name="model">Actual model <see cref="<#=      codeClass.FullName#>"/>.</param>  
     /// <returns>  
     /// The result of the conversion.  
     /// </returns>  
     public static explicit operator <#=      className#>(<#=      codeClass.FullName#> model)  
     {  
       var historyModel = new <#=      className#>();  
       <#=      historyMapping#>  
       return historyModel;  
     }  
   }  
 }  
 <#  
     //System.Diagnostics.Debugger.Break();  
     SaveOutput(fileName);  
     DeleteOldOutputs();  
   }  
                #>  
 <#+   
   private static readonly Regex _unwrapNullableRegex = new Regex(@"^System.Nullable(<|\(Of )(?<UnderlyingTypeName>.*)(>|\))$");  
   private static Type[] _primitiveList;  
   private static void InitializePrimitives()  
   {  
     if(_primitiveList == null)  
     {  
       var types = new[]  
         {  
           typeof (Enum),  
           typeof (String),  
           typeof (Char),  
           typeof (Boolean),  
           typeof (Byte),  
           typeof (Int16),  
           typeof (Int32),  
           typeof (Int64),  
           typeof (Single),  
           typeof (Double),  
           typeof (Decimal),  
           typeof (SByte),  
           typeof (UInt16),  
           typeof (UInt32),  
           typeof (UInt64),  
           typeof (DateTime),  
           typeof (DateTimeOffset),  
           typeof (TimeSpan),  
           };  
       var nullTypes = from t in types  
           where t.IsValueType  
           select typeof (Nullable<>).MakeGenericType(t);  
       _primitiveList = types.Concat(nullTypes).ToArray();  
     }  
   }  
   public string GetFriendlyPropertyTypeName(CodeProperty codeProperty)  
   {  
     var typeName = string.Empty;  
     var codeProperty2 = codeProperty as CodeProperty2;  
     if(codeProperty2.IsGeneric)  
     {  
       typeName = _unwrapNullableRegex.Replace(codeProperty.Type.AsFullName,"");  
     }  
     typeName = codeProperty2.Type.AsString;  
     return typeName.StartsWith("System") ? "global::" + typeName :typeName;  
   }  
   /// <summary>  
   /// Determines whether the supplied CodeTypeRef represents a primitive .NET type, e.g.,  
   /// byte, bool, float, etc.  
   /// </summary>  
   public static bool IsPrimitive(CodeTypeRef codeTypeRef)  
   {  
     InitializePrimitives();  
     return _primitiveList.Any(item => item.FullName == codeTypeRef.AsFullName || _unwrapNullableRegex.Match(codeTypeRef.AsFullName).Success);  
   }  
   public static bool IsNullable(CodeTypeRef codeTypeRef)  
   {  
     return _unwrapNullableRegex.Match(codeTypeRef.AsFullName).Success;  
   }  
   public static IEnumerable<CodeAttribute> GetAttributes(CodeProperty codeProperty)  
   {  
     return codeProperty.Attributes.OfType<CodeAttribute>().ToList();  
   }  
 #>  

You may need to tweak few things to adjust according to your folder structure and files.

After running above T4 this file would be automatically generated.

 //------------------------------------------------------------------------------  
 // <auto-generated>  
 //   This code was generated from a template and will be overwritten as soon   
 //       as the template is executed.  
 //  
 //   Changes to this file may cause incorrect behavior and will be lost if  
 //   the code is regenerated.  
 // </auto-generated>  
 //------------------------------------------------------------------------------  
 using System;  
 using System.ComponentModel.DataAnnotations;  
 using MyProject.Model.CustomAttribute;  
 using MyProject.Model.Enumeration;  
 using MyProject.Common.ExtensionMethods;  
 using MyProject.Model.ModalConstraint;  
 namespace MyProject.Model.DomainModel.History  
 {  
   /// <summary>  
   /// History model of <see cref="MyProject.Model.DomainModel.TestModel"/>.  
   /// </summary>  
   [ManageHistorySource(typeof(MyProject.Model.DomainModel.TestModel))]  
   public class HistoryTestModel  
   : IPrimaryKey, IModelHistory  
   {  
     /// <summary>  
     /// Gets or sets the Id.  
     /// </summary>  
     public int Id { get; set; }  
     /// <summary>  
     /// Gets or sets the Name.  
     /// </summary>  
     public string Name { get; set; }  
     #region " IModelHistory implementation "  
     /// <summary>  
     /// Gets or sets the history model identifier.  
     /// </summary>  
     /// <value>  
     /// The history model identifier.  
     /// </value>  
     public int HistoryModelId { get; set; }  
     /// <summary>  
     /// Gets or sets the user id.  
     /// </summary>  
     /// <value>The user id.</value>  
     public int? UserId { get; set; }  
     /// <summary>  
     /// Gets or sets the created at.  
     /// </summary>  
     /// <value>  
     /// The created at.  
     /// </value>  
     public DateTime CreatedAt { get; set; }  
     /// <summary>  
     /// Gets or sets the operation.  
     /// </summary>  
     /// <value>  
     /// The operation.  
     /// </value>  
     public DbOperation Operation { get; set; }  
     #endregion " IModelHistory implementation "  
     /// <summary>  
     /// Performs an explicit conversion from <see cref="MyProject.Model.DomainModel.TestModel"/> to <see cref="HistoryTestModel"/>.  
     /// </summary>  
     /// <MyProjectname="model">Actual model <see cref="MyProject.Model.DomainModel.TestModel"/>.</param>  
     /// <returns>  
     /// The result of the conversion.  
     /// </returns>  
     public static explicit operator HistoryTestModel(MyProject.Model.DomainModel.TestModel model)  
     {  
       var historyModel = new HistoryTestModel();  
       historyModel.HistoryModelId = model.Id;  
       historyModel.Name = model.Name;  
       return historyModel;  
     }  
   }  
 }  

If you see the generated history class file, T4 automatically generates operator overloading to handle parse. Through this overloading we can parse object by minimum use of reflection.

Step 4 - Automatic Fluent mapping generation for history models.

You can refer Auto fluent mapping generation for OpenAccess/Telerik DataAccess through T4 to automatically generate fluent mappings or you can do manually by yourself.

Step 6 - Create T4 to get full model name.

This T4 will generate full model name for all domain models. It would be used for getting full names of model without using any reflection to help in creating new history object.

 <#@ template debug="true" hostSpecific="true" #>  
 <#@ output extension=".cs" #>  
 <#@ Assembly Name="System.Core" #>  
 <#@ Assembly Name="System.Windows.Forms" #>  
 <#@ import namespace="EnvDTE" #>  
 <#@ import namespace="EnvDTE80" #>  
 <#@ import namespace="System" #>  
 <#@ import namespace="System.IO" #>  
 <#@ import namespace="System.Diagnostics" #>  
 <#@ import namespace="System.Linq" #>  
 <#@ import namespace="System.Collections" #>  
 <#@ import namespace="System.Collections.Generic" #>   
 <#@ include file="EF.Utility.CS.ttinclude"#>  
 <#@ include file="../T4Plugin/VisualStudioAutomationHelper.ttinclude" #>  
 <#  
      var modelNamespace = "MyProject.Model.DomainModel";  
   var modelProject = VisualStudioHelper.GetProject("MyProject.Model");  
   var allModelClasses = VisualStudioHelper.CodeModel.  
     GetAllCodeElementsOfType(modelProject.CodeModel.CodeElements, EnvDTE.vsCMElement.vsCMElementClass, false);  
      CodeGenerationTools code = new CodeGenerationTools(this);  
      var types = allModelClasses  
         .OfType<CodeClass2>().Where(clas => clas.FullName.StartsWith(modelNamespace) &&   
         !clas.FullName.EndsWith("MetadataSource")  
         ).OrderBy(clas => clas.FullName).ToList();  
 #>  
 //------------------------------------------------------------------------------  
 // <auto-generated>  
 //   This code was generated from a template and will be re-created if deleted  
 //       with default implementation.  
 // </auto-generated>  
 //------------------------------------------------------------------------------  
 namespace <#= code.VsNamespaceSuggestion()#>  
 {  
   /// <summary>  
   /// Domain model representation as enum name   
   /// </summary>  
      public enum DomainModel  
      {  
 <#  
 foreach (CodeClass2 type in types)  
 {    
 #>  
           /// <summary>  
           /// Mapped domain model with <see cref="<#=type.FullName#>"/>  
           /// </summary>  
           <#=type.Name#>,  
 <#  
 }#>  
      }  
      /// <summary>  
   /// Domain model description  
   /// </summary>  
      public static class DomainModelDescription  
      {  
           #region " Domain model name representation as string "  
 <#  
 foreach (var type in types)  
 {    
 #>  
           /// <summary>  
     /// Domain model full name for <#=type.Name#>  
     /// </summary>  
           private const string DM_<#=type.Name.ToUpper()#> = "<#=type.FullName#>";  
           /// <summary>  
     /// Domain model : <#=type.Name#>  
     /// </summary>  
           private const string DM_NAME_<#=type.Name.ToUpper()#> = "<#=type.Name#>";  
 <#  
 }#>  
           #endregion " Domain model name representation as string "  
     /// <summary>  
     /// Gets the full name for domain model.  
     /// </summary>  
     /// <param name="modelName">Name of the model.</param>  
     /// <returns>Full name of model</returns>  
        public static string GetFullName(DomainModel modelName)  
     {  
       switch(modelName)  
                {  
 <#  
 foreach (var type in types)  
 {    
 #>  
                     case DomainModel.<#=type.Name#>:  
                          return DM_<#=type.Name.ToUpper()#>;  
 <#  
 }#>  
                }  
                return null;  
           }  
           /// <summary>  
     /// Gets the full name for domain model.  
     /// </summary>  
     /// <param name="modelName">Name of the model.</param>  
     /// <returns>Full name of model</returns>  
           public static string GetFullName(string modelName)  
     {  
       switch(modelName)  
                {  
 <#  
 foreach (var type in types)  
 {   
 #>  
                     case DM_NAME_<#=type.Name.ToUpper()#>:  
                          return DM_<#=type.Name.ToUpper()#>;  
 <#  
 }#>  
                }  
                return null;  
           }  
      }  
 }  

Step 5 - Using Add/Remove/Changed event on OpenAccessContext class.

Now, whole infrastructure is ready. We are going to use all above items on context add, remove and changed events.

I am putting all codes at once then will go under core parts one-by-one.

 using MyProject.Common;  
 using MyProject.Model.CustomAttribute;  
 using MyProject.Model.DomainModel;  
 using MyProject.Model.DomainModel.Security;  
 using MyProject.Model.DomainModel.UserProfile;  
 using MyProject.Model.DTO;  
 using MyProject.Model.DTO.Artificials;  
 using MyProject.Model.Enumeration;  
 using MyProject.Model.ModalConstraint;  
 using MyProject.Model.ModelHelper;  
 using System;  
 using System.Collections.Generic;  
 using System.Linq;  
 using System.Reflection;  
 using System.Security.Principal;  
 using System.Threading;  
 using Telerik.OpenAccess;  
 using Telerik.OpenAccess.Metadata;  
 namespace MyProject.DB.Infrastructure  
 {  
   /// <summary>  
   /// OpenContext wrapper class for MyProject  
   /// </summary>  
   public sealed class MyProjectContext  
   : OpenAccessContext  
   {  
     #region " Variables "  
     /// <summary>  
     /// The back end configuration  
     /// </summary>  
     internal static BackendConfiguration BackEndConfig;  
     /// <summary>  
     /// The connection string  
     /// </summary>  
     internal static string ConnectionString;  
     /// <summary>  
     /// Gets or sets the meta source.  
     /// </summary>  
     /// <value>  
     /// The meta source.  
     /// </value>  
     private static MyProjectFluentMetadataSource MetaSource;  
     /// <summary>  
     /// Audit history to check already added items  
     /// </summary>  
     private readonly Dictionary<object, object> Audits = new Dictionary<object, object>();  
     /// <summary>  
     /// The user identifier  
     /// </summary>  
     private int? _userId;  
     /// <summary>  
     /// Gets or sets the activity log track.  
     /// </summary>  
     /// <value>  
     /// The activity log track.  
     /// </value>  
     internal ActivityLogs ActivityLogTrack { get; set; }  
     /// <summary>  
     /// Gets or sets the user principal.  
     /// </summary>  
     /// <value>  
     /// The user principal.  
     /// </value>  
     internal IPrincipal UserPrincipal { get; set; }  
     #endregion " Variables "  
     #region " Properties "  
     /// <summary>  
     /// Gets the artificial types.  
     /// </summary>  
     /// <value>  
     /// The artificial types.  
     /// </value>  
     /// <remarks>Break-ed architecture to support artificial type through context</remarks>  
           // TODO: Can be skipped if artificial types not required.  
     internal IList<ArtificialModelType> ArtificialTypes  
     {  
       get  
       {  
         return MetaSource.ArtificialModelMappings;  
       }  
     }  
     /// <summary>  
     /// Gets the user identifier.  
     /// </summary>  
     /// <value>  
     /// The user identifier.  
     /// </value>  
     private int? UserId  
     {  
       get  
       {  
         if (UserPrincipal != null && !_userId.HasValue)  
         {  
           var id = GetAll<User>()  
                           .Where(usr => usr.UserName == UserPrincipal.Identity.Name)  
                           .Select(user => user.Id)  
                           .FirstOrDefault();  
           _userId = id != 0 ? (int?)id : null;  
         }  
         return _userId;  
       }  
     }  
     #endregion " Properties "  
     /// <summary>  
     /// Initializes a new instance of the <see cref="MyProjectContext" /> class.  
     /// </summary>  
     /// <param name="connectionStringName">Name of the connection string.</param>  
     /// <param name="backendName">Name of the back-end.</param>  
     public MyProjectContext(string connectionStringName, string backendName, MyProjectFluentMetadataSource metaSource)  
       : base(ConnectionString = connectionStringName,  
       BackEndConfig = GetBackendConfiguration(backendName),  
       MetaSource = metaSource)  
     {  
       Events.Removing += (sender, evtDel) => MakeHistoryRecord(evtDel.PersistentObject, DbOperation.Delete);  
       Events.Added += MyProjectEventAdded;  
       Events.Changed += MyProjectEventChanged;  
     }  
     #region " Events "  
     /// <summary>  
     /// MyProject's context event for added items.  
     /// </summary>  
     /// <param name="sender">The sender.</param>  
     /// <param name="e">The <see cref="AddEventArgs"/> instance containing the event data.</param>  
     private void MyProjectEventAdded(object sender, AddEventArgs e)  
     {  
       // Do not process anything if object is history model  
       if (Attribute.IsDefined(e.PersistentObject.GetType(), typeof(ManageHistorySourceAttribute)))  
       {  
         return;  
       }  
       // Skip if item already exist in audit  
       // and coming from change event [after change event this is getting triggered].  
       if (Audits.ContainsKey(e.PersistentObject))  
       {  
         // set the artificial fields in case the object is already in the audit cache  
         var historyObject2 = Audits[e.PersistentObject] as IModelHistory;  
         // Set id and operation which is coming from change event.  
         if (historyObject2.HistoryModelId == 0)  
         {  
           historyObject2.HistoryModelId = GetIdOfModel(historyObject2);  
         }  
         historyObject2.Operation = DbOperation.Insert;  
         HistoryModelMappingForArtificial(e.PersistentObject, historyObject2);  
         return;  
       }  
       object historyObject;  
       if (TryParseHistoryObject(e.PersistentObject, out historyObject))  
       {  
         if (!Audits.ContainsKey(e.PersistentObject))  
         {  
           var id = GetIdOfModel(historyObject);  
           var populatedHistoryModel = PopulateModelForHistory(historyObject, id, DbOperation.Insert);  
           Audits.Add(e.PersistentObject, populatedHistoryModel);  
           Add(populatedHistoryModel);  
           HistoryModelMappingForArtificial(e.PersistentObject, populatedHistoryModel);  
         }  
       }  
     }  
     /// <summary>  
     /// MyProject's context changed event .  
     /// </summary>  
     /// <param name="sender">The sender.</param>  
     /// <param name="e">The <see cref="ChangeEventArgs"/> instance containing the event data.</param>  
     private void MyProjectEventChanged(object sender, ChangeEventArgs e)  
     {  
       // Do not process anything if object is history model  
       if (Attribute.IsDefined(e.PersistentObject.GetType(), typeof(ManageHistorySourceAttribute)))  
       {  
         return;  
       }  
       // Skip if item already exist in audit  
       if (Audits.ContainsKey(e.PersistentObject))  
       {  
         // set the artificial fields in case the object is already in the audit cache  
         var historyObject2 = Audits[e.PersistentObject] as IModelHistory;  
         HistoryModelMappingForArtificial(e.PersistentObject, historyObject2);  
         return;  
       }  
       object historyObject;  
       if (TryParseHistoryObject(e.PersistentObject, out historyObject))  
       {  
         var populatedHistoryModel = PopulateModelForHistory(historyObject, GetIdOfModel(e.PersistentObject),  
           DbOperation.Update, e.PropertyName, e.OldValue);  
         CollectChanges(e.PersistentObject, populatedHistoryModel);  
         HistoryModelMappingForArtificial(e.PersistentObject, populatedHistoryModel);  
       }  
     }  
     #endregion " Events "  
     #region " Methods "  
     /// <summary>  
     /// Gets the back-end configuration.  
     /// </summary>  
     /// <param name="backendName">Name of the back-end.</param>  
     /// <returns><see cref="BackendConfiguration"/> for MyProject configuration</returns>  
     public static BackendConfiguration GetBackendConfiguration(string backendName)  
     {  
       //TODO: Configure back-end to analyze performance  
       var backendConfiguration = new BackendConfiguration();  
       backendConfiguration.Backend = backendName;  
       backendConfiguration.ProviderName = "System.Data.SqlClient";  
       return backendConfiguration;  
     }  
     /// <summary>  
     /// Gets the type descriptor.  
     /// </summary>  
     /// <param name="fullTypeName">Full name of the type.</param>  
     /// <returns>Persistent type descriptor</returns>  
     public IPersistentTypeDescriptor GetTypeDescriptor(string fullTypeName)  
     {  
       return GetScope().PersistentMetaData.GetPersistentTypeDescriptor(fullTypeName);  
     }  
     /// <summary>  
     /// Updates the schema.  
     /// </summary>  
     internal void UpdateSchema()  
     {  
       var handler = this.GetSchemaHandler();  
       string script = null;  
       try  
       {  
         script = handler.CreateUpdateDDLScript(null);  
       }  
       catch  
       {  
         bool throwException = false;  
         try  
         {  
           handler.CreateDatabase();  
           script = handler.CreateDDLScript();  
         }  
         catch  
         {  
           throwException = true;  
         }  
         if (throwException)  
         {  
           throw;  
         }  
       }  
       if (!string.IsNullOrEmpty(script))  
       {  
         // Twice initialization for artificial types are handled on factory level.  
         handler.ForceExecuteDDLScript(script);  
       }  
     }  
     #region " History model generation "  
     /// <summary>  
     /// Collects the changes for history.  
     /// </summary>  
     /// <typeparam name="THistoryModel">The type of the history model.</typeparam>  
     /// <param name="key">The key.</param>  
     /// <param name="historyObject">The history object.</param>  
     private void CollectChanges<THistoryModel>(object key, THistoryModel historyObject)  
     {  
       object historyItem;  
       if (!Audits.TryGetValue(key, out historyItem))  
       {  
         Audits.Add(key, historyObject);  
         Add(historyObject);  
       }  
     }  
     /// <summary>  
     /// History model mapping based on artificial types.  
     /// </summary>  
     /// <typeparam name="TModel">The type of the model.</typeparam>  
     /// <typeparam name="THistoryModel">The type of the history model.</typeparam>  
     /// <param name="persistentObject">The persistent object.</param>  
     /// <param name="historyModel">The history model.</param>  
     /// <exception cref="System.ArgumentException">historyModel;Given model is incompatible with provided history model</exception>  
     private void HistoryModelMappingForArtificial<TModel, THistoryModel>(TModel persistentObject, THistoryModel historyModel)  
     {  
       var historySource =  
         historyModel.GetType().GetCustomAttribute(typeof(ManageHistorySourceAttribute))  
         as ManageHistorySourceAttribute;  
       if (historySource == null)  
       {  
         return;  
       }  
       var persistentType = persistentObject.GetType();  
       if (historySource.Source != persistentType)  
       {  
         throw new ArgumentException("historyModel", "Given model is incompatible with provided history model.");  
       }  
                // TODO: Skip if no artificial types are used.  
       var artificialModel = ArtificialTypes.FirstOrDefault(m => m.TypeName == persistentType.Name);  
       if (artificialModel != null)  
       {  
         foreach (var modelMember in artificialModel.ArtificialModelMembers)  
         {  
           // May create problem if date time having minimum value while  
           // getting value from original model, if configuration is wrongly set for artificial types.  
           historyModel.SetFieldValue(modelMember.PropertyName, persistentObject.FieldValue<object>(modelMember.PropertyName));  
         }  
       }  
     }  
     /// <summary>  
     /// Populates the model for history.  
     /// </summary>  
     /// <typeparam name="TModel">The type of the model.</typeparam>  
     /// <param name="historyObject">The history object.</param>  
     /// <param name="pkId">The primary key identifier.</param>  
     /// <param name="dbOperation">The db operation request.</param>  
     /// <param name="propertyName">Name of the property.</param>  
     /// <param name="oldValue">The old value.</param>  
     /// <returns></returns>  
     private TModel PopulateModelForHistory<TModel>(TModel historyObject, int pkId, DbOperation dbOperation,  
       string propertyName = null, object oldValue = null)  
     {  
       var historyModel = historyObject as IModelHistory;  
       if (historyModel == null)  
       {  
         throw new ArgumentException("historyObject", "History object does not implement IModelHistory");  
       }  
       var historyModelType = historyObject.GetType();  
       // Reset value to original texts  
       if (!String.IsNullOrEmpty(propertyName) && oldValue != null)  
       {  
         // Set value through reflection  
         var prop = historyModelType.GetProperty(propertyName);  
         prop.SetValue(historyObject, oldValue, null);  
       }  
       historyModel.Operation = dbOperation;  
       historyModel.UserId = UserId;  
       historyModel.HistoryModelId = pkId;  
       historyModel.CreatedAt = DateTime.UtcNow;  
       return historyObject;  
     }  
     /// <summary>  
     /// Tries the parse history object.  
     /// </summary>  
     /// <typeparam name="THistoryModel">The type of the history model.</typeparam>  
     /// <param name="persistentObject">The persistent object.</param>  
     /// <param name="historyObject">The history object.</param>  
     /// <returns>Whether value is successfully parsed </returns>  
     private bool TryParseHistoryObject<TPersistentModel, THistoryObject>(TPersistentModel persistentObject, out THistoryObject historyObject)  
     {  
       var isSuccessful = false;  
       historyObject = default(THistoryObject);  
       if (persistentObject == null)  
       {  
         return isSuccessful;  
       }  
       // Retrieve custom attribute value for history model  
       var attribHistoryModel = persistentObject.GetType()  
                            .GetCustomAttribute(typeof(ManageHistoryAttribute), true) as ManageHistoryAttribute;  
       if (attribHistoryModel == null)  
       {  
         return isSuccessful;  
       }  
       // History model string  
       var strHistoryModel = String.IsNullOrEmpty(attribHistoryModel.HistoryModelName) ? ("History" + persistentObject.GetType().Name) : attribHistoryModel.HistoryModelName;  
       // If error occurs, let it be as it is expecting history model as per attribute  
       var historyModelFullName = DomainModelDescription.GetFullName(strHistoryModel);  
       var historyModel = Activator.CreateInstance(GetType(historyModelFullName));  
       var convertMethod = historyModel.GetType().GetMethod("op_Explicit");  
       historyModel = convertMethod.Invoke(historyModel, new object[] { persistentObject });  
       historyObject = (THistoryObject)historyModel;  
       isSuccessful = true;  
       return isSuccessful;  
     }  
     #endregion " History model generation "  
     /// <summary>  
     /// Makes the create or delete history record.  
     /// </summary>  
     /// <param name="persistentObject">The persistent object.</param>  
     /// <param name="dbOperation">The db operation.</param>  
     private void MakeHistoryRecord(object persistentObject, DbOperation dbOperation)  
     {  
       // Do not process anything if object is history model  
       if (Attribute.IsDefined(persistentObject.GetType(), typeof(ManageHistorySourceAttribute)))  
       {  
         return;  
       }  
       object historyObject;  
       if (TryParseHistoryObject(persistentObject, out historyObject))  
       {  
         var populatedHistoryModel = PopulateModelForHistory(historyObject, GetIdOfModel(historyObject), dbOperation);  
         Add(populatedHistoryModel);  
       }  
     }  
     /// <summary>  
     /// Gets the identifier of model.  
     /// </summary>  
     /// <typeparam name="TModel">The type of the model.</typeparam>  
     /// <param name="model">The model.</param>  
     /// <returns>Id of model</returns>  
     private int GetIdOfModel<TModel>(TModel model)  
     {  
       if (model as IPrimaryKey != null)  
       {  
         return (model as IPrimaryKey).Id;  
       }  
       var key = CreateObjectKey(model);  
       int id = 0;  
       if (key != null && key.ObjectKeyValues != null && key.ObjectKeyValues.Any())  
       {  
         int.TryParse(Convert.ToString(key.ObjectKeyValues.FirstOrDefault().Value), out id);  
       }  
       return id;  
     }  
           /// <summary>  
     /// Gets the type.  
     /// </summary>  
     /// <param name="typeName">Name of the type.</param>  
     /// <returns>Requested type</returns>  
     private static Type GetType(string typeName)  
     {  
       if (String.IsNullOrEmpty(typeName))  
       {  
         return null;  
       }  
       var type = Type.GetType(typeName);  
       if (type != null)  
       {  
         return type;  
       }  
       foreach (var a in AppDomain.CurrentDomain.GetAssemblies())  
       {  
         type = a.GetType(typeName);  
         if (type != null)  
         {  
           return type;  
         }  
       }  
       return null;  
     }  
     #endregion " Methods "  
   }  
 }  

The Audits object will keep track of changes by using updating or inserting object as key. Since, events get triggered multiple times for same object the checks are done to avoid multiple entries. CollectChanges creates the actual history object.

The other main part is TryParseHistoryObject, which is responsible to convert persistent object to history model. It uses ManageHistoryAttribute to identify source model and creates new history object.

In all events ManageHistorySourceAttribute attribute check is done to avoid processing of history objects.

The HistoryModelMappingForArtificial function reads artificial values and sets into history model.


Comments

Popular posts from this blog

Getting started with Raspberry Pi

Raspberry Pi is a small, low powered motherboard contains 512 RAM, combined CPU and GPU. It has LAN, 2 USB, HDMI input, Audio Out, SD Card reader and S-Video connectors. We can have many Linux distribution OS on it. To configure, we just need to attach SD Card to it. SD Card could range from class 4 to class 10. In some cases Raspberry Pi could support less then class 4 cards too. It could be powered through mini USB mobile charger. Let's get started with installing OS on SD Card. There are various ways to install OS. Like we can download OSes through  http://www.raspberrypi.org/downloads  and follow the instructions given on it. There is something BerryBoot multi-boot loader through which we can have more then one OS on Raspberry Pi and boot OS according to our need.  http://www.berryterminal.com/doku.php/berryboot  instructions could be followed to install OS with very simple steps. You need to have internet connection on Raspberry Pi to install OS. It coul...

Kendo MVC Grid DataSourceRequest with AutoMapper - Advance

The actual process to make DataSourceRequest compatible with AutoMapper was explained in my previous post  Kendo MVC Grid DataSourceRequest with AutoMapper , where we had created custom model binder attribute and in that property names were changed as data models. In this post we will be looking into using AutoMapper's Queryable extension to retrieve the results based on selected columns. When  Mapper.Map<RoleViewModel>(data)  is called it retrieves all column values from table. The Queryable extension provides a way to retrieve only selected columns from table. In this particular case based on properties of  RoleViewModel . The previous approach that we implemented is perfect as far as this article ( 3 Tips for Using Telerik Data Access and AutoMapper ) is concern about performance where it states: While this functionality allows you avoid writing explicit projection in to your LINQ query it has the same fatal flaw as doing so - it prevents the qu...

Trim text in MVC Core through Model Binder

Trimming text can be done on client side codes, but I believe it is most suitable on MVC Model Binder since it would be at one place on infrastructure level which would be free from any manual intervention of developer. This would allow every post request to be processed and converted to a trimmed string. Let us start by creating Model binder using Microsoft.AspNetCore.Mvc.ModelBinding; using System; using System.Threading.Tasks; public class TrimmingModelBinder : IModelBinder { private readonly IModelBinder FallbackBinder; public TrimmingModelBinder(IModelBinder fallbackBinder) { FallbackBinder = fallbackBinder ?? throw new ArgumentNullException(nameof(fallbackBinder)); } public Task BindModelAsync(ModelBindingContext bindingContext) { if (bindingContext == null) { throw new ArgumentNullException(nameof(bindingContext)); } var valueProviderResult = bindingContext.ValueProvider.GetValue(bin...

C# Response files

Response files are similar to batch files, having some specific instruction. On execution they perform some predefined task based on instruction. Response file contains instruction to compile programs. If we have to build complex program through command line then response files are really helpful in development process. rsp is an extension for response files. By default, csc.rsp file exists under "Framework" folder Ex: C:\Windows\Microsoft.NET\Framework\v4.0.30319. csc.rsp contains long list of system references (dlls). Some contents under csc.rsp # Reference the common Framework libraries /r:Accessibility.dll /r:Microsoft.CSharp.dll /r:System.Configuration.dll /r:System.Configuration.Install.dll /r:System.Core.dll /r:System.Data.dll /r:System.Data.DataSetExtensions.dll /r:System.Data.Linq.dll .......... In same way we can have our own response file defined which might include some third party dll. Let's see an example. Suppose we have to create an appli...

main method return value

Mainly we used to write "static void main" for entry point in console application. Placement of void denotes return type. In main function we could have "int" too but what does it really mean. "int main" signifies return type as integer. The return type of main function tells about execution status of application. Even if we have specified void as return type then it would be marked as successful program execution. If we mark int as return type then we are able to control the execution status. Now, what is the benefit of making main function as int. Windows OS saves result in  %ERRORLEVEL% environment variable of OS. If we create batch file and execute application through it then we will able to get status and based on result we can trigger something else through batch file. Let's suppose we have created program called TEST.EXE. Batch file script: @echo off REM Execute main program REM TEST.EXE @if  "%ERRORLEVEL%" == "0...

Using Redis distributed cache in dotnet core with helper extension methods

Redis cache is out process cache provider for a distributed environment. It is popular in Azure Cloud solution, but it also has a standalone application to operate upon in case of small enterprises application. How to install Redis Cache on a local machine? Redis can be used as a local cache server too on our local machines. At first install, Chocolatey https://chocolatey.org/ , to make installation of Redis easy. Also, the version under Chocolatey supports more commands and compatible with Official Cache package from Microsoft. After Chocolatey installation hit choco install redis-64 . Once the installation is done, we can start the server by running redis-server . Distributed Cache package and registration dotnet core provides IDistributedCache interface which can be overrided with our own implementation. That is one of the beauties of dotnet core, having DI implementation at heart of framework. There is already nuget package available to override IDistributedCache i...

Making FluentValidation compatible with Swagger including Enum or fixed List support

FluentValidation is not directly compatible with Swagger API to validate models. But they do provide an interface through which we can compose Swagger validation manually. That means we look under FluentValidation validators and compose Swagger validator properties to make it compatible. More of all mapping by reading information from FluentValidation and setting it to Swagger Model Schema. These can be done on any custom validation from FluentValidation too just that proper schema property has to be available from Swagger. Custom validation from Enum/List values on FluentValidation using FluentValidation.Validators; using System.Collections.Generic; using System.Linq; using static System.String; /// <summary> /// Validator as per list of items. /// </summary> /// <seealso cref="PropertyValidator" /> public class FixedListValidator : PropertyValidator { /// <summary> /// Gets the valid items /// <...

Kendo MVC Grid DataSourceRequest with AutoMapper

Kendo Grid does not work directly with AutoMapper but could be managed by simple trick using mapping through ToDataSourceResult. The solution works fine until different filters are applied. The problems occurs because passed filters refer to view model properties where as database model properties are required after AutoMapper is implemented. So, the plan is to intercept DataSourceRequest  and modify names based on database model. To do that we are going to create implementation of  CustomModelBinderAttribute to catch calls and have our own implementation of DataSourceRequestAttribute from Kendo MVC. I will be using same source code from Kendo but will replace column names for different criteria for sort, filters, group etc. Let's first look into how that will be implemented. public ActionResult GetRoles([MyDataSourceRequest(GridId.RolesUserGrid)] DataSourceRequest request) { if (request == null) { throw new Argume...

Configuring Ninject, Asp.Net Identity UserManager, DataProtectorTokenProvider with Owin

It can be bit tricky to configure both Ninject and Asp.Net Identity UserManager if some value is expected from DI to configure UserManager. We will look into configuring both and also use OwinContext to get UserManager. As usual, all configuration need to be done on Startup.cs. It is just a convention but can be used with different name, the important thing is to decorate class with following attribute to make it Owin start-up: [assembly: OwinStartup(typeof(MyProject.Web.Startup))] Ninject configuration Configuring Ninject kernel through method which would be used to register under Owin. Startup.cs public IKernel CreateKernel() { var kernel = new StandardKernel(); try { //kernel.Bind<IHttpModule>().To<HttpApplicationInitializationHttpModule>(); // TODO: Put any other injection which are required. return kernel; } catch { kernel.Dispose(); thro...