# Implementation

## Database Script

Here are the script for SQL Server

**Table - AuditLog**

```SQL

CREATE TABLE [dbo].[AuditLog](

\[Id\] \[bigint\] IDENTITY\(1,1\) NOT NULL,

\[UserId\] \[int\] NOT NULL,

\[UserName\] \[varchar\]\(50\) NOT NULL,

\[Action\] \[varchar\]\(50\) NOT NULL,

\[ChangedOn\] \[datetime\] NOT NULL CONSTRAINT \[DF\_AuditLog\_ChangedOn\]  DEFAULT \(getdate\(\)\),

\[TableName\] \[varchar\]\(50\) NOT NULL,

\[RowId\] \[int\] NOT NULL,

\[Module\] \[varchar\]\(500\) NULL,

\[Page\] \[varchar\]\(500\) NULL,

\[Changes\] \[varchar\]\(max\) NULL,

CONSTRAINT [PK_AuditLog] PRIMARY KEY CLUSTERED

(

\[Id\] ASC

)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

GO

```

**Stored Procedure - Common_AuditLog**

```SQL

CREATE PROCEDURE [dbo].[Common_AuditLog]

 @UserId                  INT

,@UserName                  VARCHAR\(50\)

,@Action                  VARCHAR\(50\)

,@TableName                  VARCHAR\(50\)

,@RowId                      INT

,@Module                  VARCHAR\(50\) = NULL

,@Page                      VARCHAR\(50\) = NULL

,@Changes                  VARCHAR\(MAX\) = NULL

AS

BEGIN

INSERT INTO [dbo].[AuditLog]

       \(\[UserId\]

       ,\[UserName\]

       ,\[Action\]

       ,\[ChangedOn\]

       ,\[TableName\]

       ,\[RowId\]

       ,\[Module\]

       ,\[Page\]

       ,\[Changes\]\)

 VALUES

       \(

        @UserId    

       ,@UserName

       ,@Action

       ,GetDate\(\)    

       ,@TableName    

       ,@RowId        

       ,@Module    

       ,@Page        

       ,@Changes    

       \)

END

GO

```

Class : AuditBehavior.cs

```C#

using Serenity.ComponentModel;

using Serenity.Data;

using Serenity.Data.Mapping;

using Serenity.Services;

using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Data;

using System.Linq;

using System.Reflection;

using System.Web;

using Serenity;

using Tranzol.Administration;

namespace Tranzol.Modules.Common.BaseClass

{

/// <summary>

/// This interface is used to log the changes for Insert / Update and Delete.

/// This identify the Identity Column as Row Id \(Unique Id\) and save in Audit Table. If Identity column is not found then it use Id \(Hard Coded\) column.

/// </summary>

public interface IAuditLog

{

}



/// <summary>

/// This is used if want to store specific IdFields instead of default Identity field \(or identity field is not avail able  \).

/// </summary>

public interface IExAuditLog 

{

    /// <summary>

    /// Assign the field which need to save as reference id in Audit Log Table

    /// </summary>

    Int32Field IdField { get; }

}



public class AuditRowBehavior : IImplicitBehavior, ISaveBehavior, IDeleteBehavior

{

    const string FieldSeperator = "; ";

    string IdFieldName = null;

    public bool ActivateFor\(Row row\)

    {

        var auditLog = row as IAuditLog;

        if \(auditLog == null\)

        {

            var exauditLog = row as IExAuditLog;

            if \(exauditLog == null\)

                return false;

            else

                IdFieldName = exauditLog.IdField.PropertyName;

        }

        return true;

    }





    public void OnAfterSave\(ISaveRequestHandler handler\) { }

    public void OnAudit\(ISaveRequestHandler handler\) {

        string auditLog = "";

        AuditActionType AuditAction = AuditActionType.Delete;

        if \(handler.IsCreate\)

        {

            auditLog = GetInsertAuditLog\(handler.Row\);

            AuditAction = AuditActionType.Insert;

        }



        if \(handler.IsUpdate\)

        {

            auditLog = GetUpdateAuditLog\(handler.Row, handler.Old\);

            AuditAction = AuditActionType.Update;

        }

        ExecuteAuditLogProc\(handler.Connection, AuditAction, handler.Row, auditLog\);

    }

    public void OnBeforeSave\(ISaveRequestHandler handler\) { }

    public void OnPrepareQuery\(ISaveRequestHandler handler, SqlQuery query\) { }

    public void OnReturn\(ISaveRequestHandler handler\) { }

    public void OnSetInternalFields\(ISaveRequestHandler handler\) { }

    public void OnValidateRequest\(ISaveRequestHandler handler\) { }

    public void OnAfterDelete\(IDeleteRequestHandler handler\) { }

    public void OnAudit\(IDeleteRequestHandler handler\) {

        var auditLog = GetInsertAuditLog\(handler.Row\);

        ExecuteAuditLogProc\(handler.Connection, AuditActionType.Delete, handler.Row, auditLog\);

    }

    public void OnBeforeDelete\(IDeleteRequestHandler handler\) { }

    public void OnPrepareQuery\(IDeleteRequestHandler handler, SqlQuery query\) { }

    public void OnReturn\(IDeleteRequestHandler handler\) { }

    public void OnValidateRequest\(IDeleteRequestHandler handler\) { }



    private string GetUpdateAuditLog\(Row CurrentRow, Row OldRow\)

    {

        List<UpdateField> audiDataList = new List<UpdateField>\(\);

        var tableFields = CurrentRow.GetTableFields\(\);

        foreach \(var tfield in tableFields\)

        {

            var propInfo = CurrentRow.GetType\(\).GetProperty\(tfield.ColumnAlias\);



            if \(propInfo.GetCustomAttribute\(typeof\(IgnoreAuditLog\)\) != null\)

                continue;

            if \(propInfo.GetCustomAttribute\(typeof\(IdentityAttribute\)\) != null\)

                IdFieldName = propInfo.Name;



            if \(propInfo.PropertyType == typeof\(DateTime?\)\)

            {

                var currDate = \(\(DateTime?\)propInfo.GetValue\(CurrentRow\)\);

                var oldDate = \(\(DateTime?\)propInfo.GetValue\(OldRow\)\);

                if \(\(oldDate.HasValue \|\| currDate.HasValue\) && oldDate != currDate\)

                    audiDataList.Add\(new UpdateField { F = propInfo.Name, V = \(currDate.HasValue ? currDate.ToIndianDateFormat\(\) : "<empty>"\), O = \(oldDate.HasValue ? oldDate.ToIndianDateFormat\(\) : "<empty>"\) }\);

            }

            else if \(propInfo.PropertyType == typeof\(Decimal?\)\)

            {

                var currValue = \(\(Decimal?\)propInfo.GetValue\(CurrentRow\)\);

                var oldValue  = \(\(Decimal?\)propInfo.GetValue\(OldRow\)\);

                if \(\(currValue.HasValue \|\| oldValue.HasValue\) && \(Convert.ToDecimal\(currValue\) - Convert.ToDecimal\(oldValue\)\) != 0\)

                    audiDataList.Add\(new UpdateField { F = propInfo.Name, O = \(oldValue.HasValue ? oldValue.Value.ToString\(\) : "<empty>"\), V = \(currValue.HasValue ? currValue.Value.ToString\(\) : "<empty>"\) }\);

            }

            else

            {

                var currValue = Convert.ToString\(propInfo.GetValue\(CurrentRow\)\);

                var oldValue = Convert.ToString\(propInfo.GetValue\(OldRow\)\);

                if \(currValue != oldValue\)

                    audiDataList.Add\(new UpdateField { F = propInfo.Name, O = \(oldValue.IsNullOrEmpty\(\) ? "<empty>" : oldValue\), V = \(currValue.IsNullOrEmpty\(\) ? "<empty>" : currValue\) }\);

            }

        }

        //return auditLog;

        return audiDataList.ToJson\(\);;

    }

    private string GetInsertAuditLog\(Row CurrentRow\)

    {

        List<InsertField> audiDataList = new List<InsertField>\(\);

        var tableFields = CurrentRow.GetTableFields\(\);

        foreach \(var tfield in tableFields\)

        {

            var propInfo = CurrentRow.GetType\(\).GetProperty\(tfield.ColumnAlias\);

            if \(propInfo.GetCustomAttribute\(typeof\(IgnoreAuditLog\)\) != null\)

                continue;

            if \(propInfo.GetCustomAttribute\(typeof\(IdentityAttribute\)\) != null\)

                IdFieldName = propInfo.Name;

            //Check if field is of DateTime type to convert it in desired format.

            if \(propInfo.PropertyType == typeof\(DateTime?\)\)

            {

                var currDate = \(\(DateTime?\)propInfo.GetValue\(CurrentRow\)\);

                if \(currDate.HasValue\)

                    audiDataList.Add\(new InsertField { F = propInfo.Name, V = currDate.ToIndianDateFormat\(\) }\);

            }

            else

            {

                var fieldValue = Convert.ToString\(propInfo.GetValue\(CurrentRow\)\);

                //Insert in log if values is defined for field.

                if \(!fieldValue.IsNullOrEmpty\(\)\)

                    audiDataList.Add\(new InsertField { F = propInfo.Name, V = fieldValue}\);

            }

        }

        return audiDataList.ToJson\(\);

    }

    public void ExecuteAuditLogProc\(IDbConnection Connection, AuditActionType AuditAction, Row CurrentRow, string AuditLog\)

    {

        using \(IDbCommand command = Connection.CreateCommand\(\)\)

        {

            IDbDataParameter pUserId = command.CreateParameter\(\);

            pUserId.ParameterName = "@UserId";

            pUserId.Value =  \(\(UserDefinition\)Authorization.UserDefinition\).UserId;

            command.Parameters.Add\(pUserId\);



            IDbDataParameter pUserName = command.CreateParameter\(\);

            pUserName.ParameterName = "@UserName";

            pUserName.Value = \(\(UserDefinition\)Authorization.UserDefinition\).Username; 

            command.Parameters.Add\(pUserName\);



            IDbDataParameter pAction = command.CreateParameter\(\);

            pAction.ParameterName = "@Action";

            pAction.Value = AuditAction.GetEnumText\(\);

            command.Parameters.Add\(pAction\);



            IDbDataParameter pTableName = command.CreateParameter\(\);

            pTableName.ParameterName = "@TableName";

            pTableName.Value = CurrentRow.Table;

            command.Parameters.Add\(pTableName\);



            IDbDataParameter pRowId = command.CreateParameter\(\);

            pRowId.ParameterName = "@RowId";

            pRowId.Value = Convert.ToString\(CurrentRow.GetType\(\).GetProperty\(IdFieldName == null ? "Id" : IdFieldName\).GetValue\(CurrentRow\)\);

            command.Parameters.Add\(pRowId\);



            IDbDataParameter pModule = command.CreateParameter\(\);

            pModule.ParameterName = "@Module";

            pModule.Value = CurrentRow.GetType\(\).Name;

            command.Parameters.Add\(pModule\);



            IDbDataParameter pPage = command.CreateParameter\(\);

            pPage.ParameterName = "@Page";

            //Sometimes same module is used for different pages for reusablity. So save page url to know the exact action page.

            pPage.Value = GetPageUrl\(\);

            command.Parameters.Add\(pPage\);



            IDbDataParameter pChanges = command.CreateParameter\(\);

            pChanges.ParameterName = "@Changes";

            pChanges.Value = AuditLog;

            command.Parameters.Add\(pChanges\);



            command.CommandType = CommandType.StoredProcedure;

            command.CommandText = "Common\_AuditLog";

            command.ExecuteNonQuery\(\);

        }

    }



    string GetPageUrl\(\)

    {

        string pageUrl = "";

        if \(HttpContext.Current != null && HttpContext.Current.Request != null\)

        {

            var httpRequest = HttpContext.Current.Request;

            if \(httpRequest.UrlReferrer != null\)

                pageUrl = httpRequest.UrlReferrer.PathAndQuery;

            else if \(httpRequest.Url != null\)

                pageUrl = httpRequest.Url.PathAndQuery;

        }

        return pageUrl;

    }

}



\[EnumKey\("Enum.Audit.AuditActionType"\)\]

public enum AuditActionType

{

    \[Description\("INSERT"\)\]

    Insert = 1,

    \[Description\("UPDATE"\)\]

    Update = 2,

    \[Description\("DELETE"\)\]

    Delete = 3

}



/// <summary>

/// Any field which does not required to log in audit table. For Example InsertUserId, InsertDate etc

/// </summary>

public class IgnoreAuditLog : Attribute

{

}



/\*Keep the field name of only one character to save space \(string length\) while converting this to json\*/

public class InsertField

{

    //Field Name

    public string F { get; set; }

    //Field Value

    public string V { get; set; }

}

public class UpdateField 

{

    //Field Name

    public string F { get; set; }

    //Old Value

    public string O { get; set; }

    //Current Value

    public string V { get; set; }

}



public static class ExtensionMethods

{

    public static string ToIndianDateFormat\(this DateTime? dateTime\)

    {

        if \(!dateTime.HasValue\)

            return "";

        return dateTime.Value.ToString\("dd/MM/yyyy"\);

    }



    public static string GetEnumText\(this object EnumObj\)

    {

        Type type = EnumObj.GetType\(\);

        if \(!type.IsEnum\)

        {

            throw new ArgumentException\("EnumerationValue must be of Enum type", "EnumObj"\);

        }



        //Tries to find a DescriptionAttribute for a potential friendly name

        //for the enum

        MemberInfo\[\] memberInfo = type.GetMember\(EnumObj.ToString\(\)\);

        if \(memberInfo != null && memberInfo.Length > 0\)

        {

            object\[\] attrs = memberInfo\[0\].GetCustomAttributes\(typeof\(DescriptionAttribute\), false\);



            if \(attrs != null && attrs.Length > 0\)

            {

                //Pull out the description value

                return \(\(DescriptionAttribute\)attrs\[0\]\).Description;

            }

        }

        //If we have no description attribute, just return the ToString of the enum

        return EnumObj.ToString\(\);



    }

}

}

```

#How to use

  1. Run Table and Stored Procedure script in your DB and create a cs file by copying the above code.

  2. Rename 'Tranzol' to your project name in following using statement to fix compilation error

    ```using Tranzol.Administration;```

  3. Inherit your RowClass with interface IAuditLog. This will automatically pic the identity column to have insert in audit log.

```c#

[ConnectionKey("Default"), DisplayName("Users"), InstanceName("User"), TwoLevelCached]

\[ModifyPermission\(Administration.PermissionKeys.Security\)\]

\[LookupScript\("Administration.User"\)\]

public sealed class UserRow : LoggingRow, IIdRow, INameRow, IIsActiveRow, \*\*IAuditLog\*\*

{

    \[DisplayName\("User Id"\), Identity, QuickSearch\]

    public Int32? UserId

    {

        get { return Fields.UserId\[this\]; }

        set { Fields.UserId\[this\] = value; }

    }

}

```

  1. If you do not have Identity Column or need any other column to keep reference in audit table then use IExAuditLog and assign the field.
  1. If you want not to log any field in audit then use IgnoreAuditLog attribute on the field.

For example: Do not need to log logging field like InsertUserId, InsertDate, UpdateUserId, UpdateDate because that will already tracked by audit table.

```c#

[NotNull, Insertable(false), Updatable(false), IgnoreAuditLog]

    \[ForeignKey\("\[dbo\].\[Users\]", "Id"\), LeftJoin\("jCreatedByUserDetails"\), TextualField\("CreatedByUserName"\)\]

    public Int32? InsertUserId

    {

        get { return loggingFields.InsertUserId\[this\]; }

        set { loggingFields.InsertUserId\[this\] = value; }

    }

```

#Output

All changes are logged in json format. Here is the screenshot of the audit log -

![Audit Log Output](http://demo.tranzol.in/HappyAuditing.png\)

results matching ""

    No results matching ""