Audit logs in the Html5 client or cloud business app

Introduction

I have written in the pas about Audit Logging.

In this post I present a way how audit logs can be consumed in the html 5 client or in a cloud business app.

Let’s start with a breaking change

In my initial audit logging solution, I provided the ability to audit log also external database table which potentially had no primary key based on a simple ID field.

So, that why in the audit table there was a Key Segment field which could look quite complicated in the case of a complex compound key.

I’m not sure if this feature had that many added value. As a result we presume now that each table has a primary key based on an simple Id field with the name “Id”.

The simplified server code for audit logging

 

using Microsoft.LightSwitch;
using Microsoft.LightSwitch.Details;
using Microsoft.LightSwitch.Framework;
using Microsoft.LightSwitch.Model;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Web;

namespace LightSwitchApplication
{
    public static class AuditHelper
    {
        public static void CreateAuditTrailForUpdate<E>(E entity, EntitySet<AuditTrail> auditTrailEntitySet) where E : IEntityObject
        {
            StringBuilder newValues = new StringBuilder();
            StringBuilder oldValues = new StringBuilder();

            foreach (var prop in entity.Details.Properties.All().OfType<IEntityStorageProperty>())
            {
                if (!prop.Name.Equals("RowVersion") && !(Object.Equals(prop.Value, prop.OriginalValue)))
                {
                    oldValues.AppendLine(string.Format("{0}: {1}", prop.Name, prop.OriginalValue));
                    newValues.AppendLine(string.Format("{0}: {1}", prop.Name, prop.Value));
                }
            }

            foreach (var prop in entity.Details.Properties.All().OfType<IEntityReferenceProperty>())
            {
                if (!prop.Name.Equals("RowVersion") && !(Object.Equals(prop.Value, prop.OriginalValue)))
                {
                    oldValues.AppendLine(string.Format("{0}: {1}", prop.Name, prop.OriginalValue));
                    newValues.AppendLine(string.Format("{0}: {1}", prop.Name, prop.Value));
                }
            }

            CreateAuditRecord<E>(entity, auditTrailEntitySet, newValues, oldValues, "Updated");
        }


        public static void CreateAuditTrailForInsert<E>(E entity, EntitySet<AuditTrail> auditTrailEntitySet) where E : IEntityObject
        {
            StringBuilder newValues = new StringBuilder();
            StringBuilder oldValues = new StringBuilder("The record has been newly created");

            foreach (var prop in entity.Details.Properties.All().OfType<IEntityStorageProperty>())
            {
                if (!prop.Name.Equals("RowVersion"))
                    newValues.AppendLine(string.Format("{0}: {1}", prop.Name, prop.Value));
            }

            foreach (var prop in entity.Details.Properties.All().OfType<IEntityReferenceProperty>())
            {
                if (!prop.Name.Equals("RowVersion"))
                    newValues.AppendLine(string.Format("{0}: {1}", prop.Name, prop.Value));
            }

            var auditRecord = CreateAuditRecord<E>(entity, auditTrailEntitySet, newValues, oldValues, "Inserted");

            //savechanges needs only be done if the datasource of the entity under audit is different from the datasource of the audittable itself
            string auditTableDataSource = auditRecord.Details.EntitySet.Details.DataService.Details.Name;
            if (auditRecord.DataSource != auditTableDataSource)
            {
                auditRecord.Details.EntitySet.Details.DataService.SaveChanges();
            }
        }

        public static void CreateAuditTrailForDelete<E>(E entity, EntitySet<AuditTrail> auditTrailEntitySet) where E : IEntityObject
        {
            StringBuilder oldValues = new StringBuilder();
            StringBuilder newValues = new StringBuilder("The record has been deleted");

            foreach (var prop in entity.Details.Properties.All().OfType<IEntityStorageProperty>())
            {

                if (!prop.Name.Equals("RowVersion"))
                    oldValues.AppendLine(string.Format("{0}: {1}", prop.Name, prop.Value));
            }

            foreach (var prop in entity.Details.Properties.All().OfType<IEntityStorageProperty>())
            {

                if (!prop.Name.Equals("RowVersion"))
                    oldValues.AppendLine(string.Format("{0}: {1}", prop.Name, prop.Value));
            }

            CreateAuditRecord<E>(entity, auditTrailEntitySet, newValues, oldValues, "Deleted");
        }
        private static AuditTrail CreateAuditRecord<E>(E entity, EntitySet<AuditTrail> auditTrailEntitySet, StringBuilder newValues, StringBuilder oldValues, string changeType) where E : IEntityObject
        {
            AuditTrail auditRecord = auditTrailEntitySet.AddNew();
            auditRecord.ChangeType = changeType;
            auditRecord.KeySegment = entity.Details.Properties["Id"].Value.ToString();// SerializeKeySegments(entity);
            auditRecord.ReferenceType = entity.Details.EntitySet.Details.Name;
            auditRecord.DataSource = entity.Details.EntitySet.Details.DataService.Details.Name;
            auditRecord.Updated = DateTime.Now;
            auditRecord.ChangedBy = Application.Current.User.FullName;
            if (oldValues != null)
            {
                auditRecord.OriginalValues = oldValues.ToString();
            }
            if (newValues != null)
            {
                auditRecord.NewValues = newValues.ToString();
            }

            return auditRecord;
        }

    }
}

 Application Specific server side code

The ApplicationDataService class goes as follows:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.LightSwitch;
using Microsoft.LightSwitch.Security.Server;
namespace LightSwitchApplication
{
    public partial class ApplicationDataService
    {
        partial void SaveChanges_Executing()
        {
            EntityChangeSet changes = this.DataWorkspace.ApplicationData.Details.GetChanges();

            foreach (var modifiedItem in changes.ModifiedEntities)
            {
                AuditHelper.CreateAuditTrailForUpdate(modifiedItem, this.AuditTrails);
            }

            foreach (var deletedItem in changes.DeletedEntities)
            {
                AuditHelper.CreateAuditTrailForDelete(deletedItem, this.AuditTrails);
            }
        }

        //the _Inserted method needs to be repeated for all entities where auditing is desired
        partial void Customers_Inserted(Customer entity)
        {
            AuditHelper.CreateAuditTrailForInsert(entity, this.AuditTrails);
        }

        partial void GetAuditTrailsByKeySegmentAndEntitySet_PreprocessQuery(string keySegment, string entitySet, ref IQueryable<AuditTrail> query)
        {
            if (!string.IsNullOrEmpty(keySegment) && !string.IsNullOrEmpty(entitySet))
            {
                query = query.Where(a => a.KeySegment == keySegment && a.ReferenceType == entitySet);
            }
        }
    }
}


Make sure to add a query named “GetAuditTrailsByKeySegmentAndEntitySet with 2 string parameters: keySegment and entitySet.

The user interface

The full audit log

Quite obvious to generate of course. Simply a browse screen on the

image

 

Audit Detail

image

 

A more interesting feature here is to be able to jump directly back to the original record, by means of the “Show Original Entity” button.

/// <reference path="~/GeneratedArtifacts/viewModel.js" />

myapp.ViewAuditTrail.ShowOriginalEntity_execute = function (screen) {
var hashPos = window.location.href.indexOf("#");
 if (hashPos >= 0)
var baseUrl = window.location.href.substr(0, hashPos);
 else
var baseUrl = window.location.href;

var entity = screen.AuditTrail;

var id = entity.KeySegment;
var entitySetName = entity.ReferenceType;
var dataServiceName = entity.DataSource;
 //It could make sense to check first if record still exists, by default the deep linking will bring you  to the browse screen
var fullUrl = baseUrl + "?entity=" + dataServiceName + "/" + entitySetName + "(" + id + ")";

window.location.href = fullUrl;
 };

Show audit details from entity detail screen

The other way around can be also useful:

image

 

Show audit records

The previous show audit records button brings us to the filtered list of audit records for the specific entity:

 

myapp.ViewCustomer.ShowAuditRecords_execute = function (screen) {
ShowRelatedAuditRecords(screen.Customer)
 };
function ShowRelatedAuditRecords(entity) {
 var entitySet = entity.details.entitySet.name;
 var keySegment = entity.Id.toString();
myapp.showBrowseAuditTrails(keySegment, entitySet);
 };

Conclusion

Easy to set up, as always in LightSwitch.