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

Elegantly dealing with TimeZones in MVC Core / WebApi

In any new application handling TimeZone/DateTime is mostly least priority and generally, if someone is concerned then it would be handled by using DateTime.UtcNow on codes while creating current dates and converting incoming Date to UTC to save on servers. Basically, the process is followed by saving DateTime to UTC format in a database and keep converting data to native format based on user region or single region in the application's presentation layer. The above is tedious work and have to be followed religiously. If any developer misses out the manual conversion, then that area of code/view would not work. With newer frameworks, there are flexible ways to deal/intercept incoming or outgoing calls to simplify conversion of TimeZones. These are steps/process to achieve it. 1. Central code for storing user's state about TimeZone. Also, central code for conversion logic based on TimeZones. 2. Dependency injection for the above class to be able to use global

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 /// <

Handling JSON DateTime format on Asp.Net Core

This is a very simple trick to handle JSON date format on AspNet Core by global settings. This can be applicable for the older version as well. In a newer version by default, .Net depends upon Newtonsoft to process any JSON data. Newtonsoft depends upon Newtonsoft.Json.Converters.IsoDateTimeConverter class for processing date which in turns adds timezone for JSON data format. There is a global setting available for same that can be adjusted according to requirement. So, for example, we want to set default formatting to US format, we just need this code. services.AddMvc() .AddJsonOptions(options => { options.SerializerSettings.DateTimeZoneHandling = "MM/dd/yyyy HH:mm:ss"; });

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 ArgumentNullExce

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

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 query result from

OpenId Authentication with AspNet Identity Core

This is a very simple trick to make AspNet Identity work with OpenId Authentication. More of all both approach is completely separate to each other, there is no any connecting point. I am using  Microsoft.AspNetCore.Authentication.OpenIdConnect  package to configure but it should work with any other. Configuring under Startup.cs with IAppBuilder app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme, LoginPath = new PathString("/Account/Login"), CookieName = "MyProjectName", }) .UseIdentity() .UseOpenIdConnectAuthentication(new OpenIdConnectOptions { ClientId = "<AzureAdClientId>", Authority = String.Format("https://login.microsoftonline.com/{0}", "<AzureAdTenant>"), ResponseType = OpenIdConnectResponseType.IdToken, PostLogoutRedirectUri = "<my website url>",

Data seed for the application with EF, MongoDB or any other ORM.

Most of ORMs has moved to Code first approach where everything is derived/initialized from codes rather than DB side. In this situation, it is better to set data through codes only. We would be looking through simple technique where we would be Seeding data through Codes. I would be using UnitOfWork and Repository pattern for implementing Data Seeding technique. This can be applied to any data source MongoDB, EF, or any other ORM or DB. Things we would be doing. - Creating a base class for easy usage. - Interface for Seed function for any future enhancements. - Individual seed classes. - Configuration to call all seeds. - AspNet core configuration to Seed data through Seed configuration. Creating a base class for easy usage public abstract class BaseSeed<TModel> where TModel : class { protected readonly IMyProjectUnitOfWork MyProjectUnitOfWork; public BaseSeed(IMyProjectUnitOfWork MyProjectUnitOfWork) { MyProject

MongoDB navigation property or making it behave as ORM in .Net

This is an implementation to make models to have  navigation properties work like ORM does for us. What actually happens in ORM to make navigation properties work? Entity Framework has proxy classes implementation to allow lazy loading and eager loading implementation. While creating proxy classes it also changes definitions for actual classes to make navigation properties work to get values based on Model's for navigation properties. Most of ORMs work in same fashion like Telerik DataAccess has enhancer tool which changes class definition at compile time to enable navigation properties. In this implementation, we would retain the original class but we would have extension methods to allow initializing properties to have navigation proprieties work. Let's first create desire model on which we need to implement. I am picking up simple one-to-many relationship example from Person to Address. public class Person { public int PersonId { get; set; }