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.


Popular posts from this blog

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"; });



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 globally. 3. Creating Mo…

Architecture solution composting Repository Pattern, Unit Of Work, Dependency Injection, Factory Pattern and others

Project architecture is like garden, we plant the things in certain order and eventually they grow in similar manner. If things are planted well then they will all look(work) great and easier to manage. If they grow as cumbersome it would difficult to maintain and with time more problems would be happening in maintenance.

There is no any fixed or known approach to decide project architecture and specially with Agile Methodology. In Agile Methodology, we cannot predict how our end products will look like similarly we cannot say a certain architecture will fit well for entire development lifespan for project. So, the best thing is to modify the architecture as per our application growth. I understand that it sounds good but will be far more problematic with actual development. If it is left as it is then more problems will arise with time. Just think about moving plant vs a full grown tree.

Coming to technical side, In this article, I will be explaining about the various techniques tha…

LDAP with ASP.Net Identity Core in MVC with project.json

Lightweight Directory Access Protocol (LDAP), the name itself explain it. An application protocol used over an IP network to access the distributed directory information service.

The first and foremost thing is to add references for consuming LDAP. This has to be done by adding reference from Global Assembly Cache (GAC) into project.json

"frameworks": { "net461": { "frameworkAssemblies": { "System.DirectoryServices": "4.0.0.0", "System.DirectoryServices.AccountManagement": "4.0.0.0" } } },
These System.DirectoryServices and System.DirectoryServices.AccountManagement references are used to consume LDAP functionality.

It is always better to have an abstraction for irrelevant items in consuming part. For an example, the application does not need to know about PrincipalContext or any other dependent items from those two references to make it extensible. So, we can begin with some bas…

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(bindingC…

Unit Of Work injection through Asp.Net Core Dependency Injection

This article is not directly related to UnitOfWork but leveraging Asp.Net Core Dependency Injection to consume Unit Of Work.

In one of the previous article about project architecture, I was not very satisfied with the approach for Unit Of Work implementation for initialization of repository even if with some advantage.

Here is old code for UnitOfWork.

public sealed partial class MyProjectUnitOfWork : UnitOfWork<DbContext>, IMyProjectUnitOfWork { public MyProjectUnitOfWork(IContextFactory<DbContext> contextFactory) : base(contextFactory) { } /// <summary> /// BookRepository holder /// </summary> private MyProject.DB.Repository.BookRepository _bookRepository; /// <summary> /// Gets the BookRepository repository. /// </summary> /// <value> /// The BookRepository repository. /// </value> MyProject.Interface.Repository.IBoo…

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

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 being cached.
Since …

jQuery Datatable generic implementation on .Net/dotnet Core for any entity server filtering or sorting through EF/EF Core

jQuery Datatable is one of the popular freely available grid for the developers. It works really great on the client side but there is always need to write a lot of codes on the server side for filtering and sorting for each individual entities.

In this article, we would address this by creating a generic implementation of server code which can work on any entity without writing any further piece of code for filtering and sorting. Just by calling a generic extension method it would resolve sorting and filtering.

The key points to achieve it are as follows:
- Model binding and provider to transform client side request to strongly typed item on the server.
- Once we get strongly typed items to read values, we can apply a dynamic approach to generate queries and filters through LINQ expression.
- At final part, just consumption of created mechanism by creating endpoints for each entity. Also, we can easily select required columns through LINQ projection.
Model Binding As per DataTable ser…

T4, Generating interface automatically based on provided classes

With new techniques and patterns interface plays a key role in application architecture. Interface makes application extendable like defining file upload interface and implementing based on file system, Azure Blob storage, Amazon S3. At starting we might be implementing based on Azure Blob but later we might move to Windows based file system and so on.

Ideally we create interface based on need and start implementing actual default implementation class. Many a times at starting of implementation there is one to one mapping between Interface and Class. Like from above example File upload interface and the initial or default class implementation that we design and with time it will get extended.
In this article, we will try to create interface based on default class implementation. This is not at all recommended in Test Driven Design (TDD) where we test the application before actual code implementation but I feel sometimes and in some situations it is okay do that and test straight afte…