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.
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:
An interface to have common Id column which acts as primary key for all models. Can be generic as well.
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.
DbOperation is enum for knowing type of operation is done on history table.
Step 3 - Custom attribute for history models to know original source item.
This attribute would be used on history models to know source item.
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.
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.
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.
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.
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.
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.
- 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
Post a Comment