Skip to main content
Version: 0.43

Audit Logging

Shesha's audit trail is built on top of Abp.EntityHistory, with a number of Shesha-specific additions layered on top: a ready-made DataTable view of an entity's history, extension methods for logging custom events, and attributes that control how property changes and related-entity changes are described.


Audit Trail DataTable Configuration

Implemented in the Shesha.Application module's EntityHistoryAppService class (GetAuditTrailAsync). Use it to show the audit trail for a specific entity in a DataTable control.

Columns

ColumnDescription
Type of eventType of audited event (Created, Updated, Child object updated, child object added, etc.) or changes of properties
DescriptionMain description of the event - the changed properties, the name of an added or removed child entity, or a custom event description. If several properties changed at once, all the changes are shown as a single description
UserFull name of the user who made the change or triggered the event
DateDate of the event

Entity History for Anonymous Users (e.g. a Background Job)

By default, changes made by anonymous users (such as a background job with no logged-in user) are not recorded. Add the following to PostInitialize in SheshaWebHostModule.cs (in the Shesha.Web.Host module's Startup folder) to enable it:

public override void PostInitialize()
{
Configuration.EntityHistory.IsEnabledForAnonymousUsers = true;
}

Adding Custom Audit Events

These extension methods, defined in Shesha.NHibernate.EntityHistory.EntityHistoryExtension, let you add any custom event to an entity's audit trail.

A Simple Event With a Description

public static void AddHistoryEvent(this object entity, string description)

Example:

user.Password = newPassword;
user.AddHistoryEvent("Password reset");

An Event With a Type and a Description

public static void AddHistoryEvent(this object entity, string eventName, string description)

Example:

user.Password = newPassword;
user.AddHistoryEvent("Password reset", "Password reset by Administrator");

A Custom Property Change Description

Use a custom description for a specific property change instead of the standard description.

public static void AddPropertyChangeDescription(this object entity, string description, string propertyName)
// or
public static void AddPropertyChangeDescription<TModel, TValue>(this TModel entity, string description, Expression<Func<TModel, TValue>> property)

Example:

user.IsActive = false;
user.AddPropertyChangeDescription("User inactivated", "IsActive");
// or
user.IsActive = false;
user.AddPropertyChangeDescription("User inactivated", p => p.IsActive);

This change is shown in the audit trail as User inactivated, instead of "IsActive" was changed from "true" to "false".


A Property Change Comment

A custom comment for a specific property change, added after the standard description rather than replacing it.

public static void AddPropertyChangeComment(this object entity, string description, string propertyName)
// or
public static void AddPropertyChangeComment<TModel, TValue>(this TModel entity, string description, Expression<Func<TModel, TValue>> property)

Example:

user.IsActive = false;
user.AddPropertyChangeComment("User inactivated", "IsActive");
// or
user.AddPropertyChangeComment("User inactivated", p => p.IsActive);

This change is shown in the audit trail as "IsActive" was changed from "true" to "false" (User inactivated).


Property-Level Audit Attributes

Audited Boolean Values

Show a custom message instead of the standard property-change description when a boolean property changes.

[AttributeUsage(AttributeTargets.Property)]
public class AuditedBooleanAttribute : Attribute
{
public string EventText { get; set; }
public string TrueText { get; set; }
public string FalseText { get; set; }

public AuditedBooleanAttribute(string trueText, string falseText)
}

Example:

[AuditedBoolean("SMS Based One-Time-Passwords enabled", "SMS Based One-Time-Passwords disabled")]
public virtual bool OtpEnabled { get; set; }

This is shown in the audit trail as SMS Based One-Time-Passwords enabled (instead of "OtpEnabled" was changed from "false" to "true"), or as SMS Based One-Time-Passwords disabled for the reverse change.


Audit a Property Change as a Custom Event

Show a custom description and event type, built by a class that inherits from EntityHistoryEventCreatorBase.

[AttributeUsage(AttributeTargets.Property)]
public class AuditedAsEventAttribute : Attribute
{
public Type EventCreator { get; set; }
public bool SaveFullInfo { get; set; }

public AuditedAsEventAttribute(Type eventCreator, bool saveFullInfo = true)
}

Example:

private class SchoolInformationStatusEventCreator : EntityHistoryEventCreatorBase<RefListSchoolInformationStatus>
{
public override EntityHistoryEventInfo CreateEvent(EntityChangesInfo<RefListSchoolInformationStatus> change)
{
return CreateEvent("School information status changed",
change.NewValue == RefListSchoolInformationStatus.Submitted
? "Submitted for verification"
: change.NewValue == RefListSchoolInformationStatus.Approved
? "Submitted as final"
: "Not submitted");
}
}

[AuditedAsEvent(typeof(SchoolInformationStatusEventCreator))]
public virtual RefListSchoolInformationStatus? SchoolInformationStatus { get; set; }

This is shown in the audit trail as School information status changed: Not submitted, School information status changed: Submitted for verification, or School information status changed: Submitted as final, instead of the raw "SchoolInformationStatus" was changed from "X" to "Y".


These class-level attributes pull audit events from related or child entities into the audit trail of the entity being viewed.

[AttributeUsage(AttributeTargets.Property)]
public class DisplayChildAuditTrailAttribute : Attribute
{
public string[] AuditedFields { get; set; }
}

AuditedFields is the array of child-entity field names to display. If provided, you also need to specify which action types (Created, Updated, Deleted) should be shown.

Example:

[DisplayChildAuditTrail]
[Display(Name = "Parent")]
public virtual Parent SubmittedBy { get; set; }

Shows changes to the related Parent entity, reached through the SubmittedBy field.


Events From Generic Child Entities (e.g. Notes, Attachments, Notifications)

Generic entities typically implement IEntityWithMultipleOwnerTypes.

Watch the spelling

This attribute is spelled DisplayGenericEntitesAuditTrailAttribute - missing the "i" in "Entites" - not the more natural-looking DisplayGenericEntitiesAuditTrailAttribute. Your code needs to match this exact spelling, as shown below, or it won't compile.

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class DisplayGenericEntitesAuditTrailAttribute : Attribute
{
public Type EntityType { get; set; }
public string OwnerIdField { get; set; }
public string OwnerTypeField { get; set; }
public string DisplayName { get; set; }
public string NameField { get; set; }
public string CategoryField { get; set; }
public object CategoryValue { get; set; }

public DisplayGenericEntitesAuditTrailAttribute(Type entityType)
}
  • EntityType - type of the related generic entity (e.g. StoredFile, Note, Comment)
  • OwnerIdField - field on the generic entity holding the owner's ID. Defaults to OwnerId
  • OwnerTypeField - field on the generic entity holding the owner's type. Defaults to OwnerType
  • DisplayName - the type name shown in the audit trail. If empty, uses EntityAttribute()?.FriendlyName of EntityType, or a friendly name derived from the class name
  • NameField - field on the generic entity used as its display name. If empty, looks for a field with EntityDisplayNameAttribute, or falls back to .ToString()
  • CategoryField / CategoryValue - optionally restrict this to generic entities where CategoryField equals CategoryValue (for example, showing only Note entities of a specific category)

Example:

[DisplayGenericEntitesAuditTrail(typeof(Comment), NameField = "Text")]
public class SchoolApplication : FullAuditedEntity<Guid>

Shows events from related Comment entities, using the comment's Text field as the event's description.


The join entity must implement IFullAudited.

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class DisplayManyToManyAuditTrailAttribute : Attribute
{
public Type ManyToManyEntityType { get; set; }
public string DisplayName { get; set; }
public string OwnEntityField { get; set; }
public string RelatedEntityField { get; set; }
public Type RelatedEntityType { get; set; }
public bool AnyRelatedEntityType { get; set; }
public string NameField { get; set; }
public string[] AuditedFields { get; set; }

public DisplayManyToManyAuditTrailAttribute(Type manyToManyEntityType, string relatedEntityField)
}
  • ManyToManyEntityType - the join entity's type
  • DisplayName - defaults to the join entity type's friendly name
  • OwnEntityField - field on the join entity referencing this entity. If empty, Shesha looks for a field of this entity's own type
  • RelatedEntityField - field on the join entity referencing the related entity
  • RelatedEntityType - the related entity's type (optional, useful when the join entity's related type is itself a base type)
  • AnyRelatedEntityType - when true, matches related entities of any type reachable through RelatedEntityField (useful for an inherited hierarchy, e.g. Employee extends Person)
  • NameField - field on the related entity used as its display name
  • AuditedFields - as above, the array of child-entity fields to display alongside the required action types

Example:

public class ShaRoleAppointedPerson : FullAuditedEntity<Guid>
{
public virtual Person Person { get; set; }
public virtual ShaRole Role { get; set; }
}

[DisplayManyToManyAuditTrail(typeof(ShaRoleAppointedPerson), "Person", DisplayName = "Member")]
public class ShaRole : FullAuditedEntity<Guid>, IMayHaveTenant

Shows events from related Person entities as Member changes, using ShaRoleAppointedPerson as the join entity - for example, "Member" added: <Full Name of person>.

The reverse relationship can be declared on the other side too:

[DisplayManyToManyAuditTrail(typeof(ShaRoleAppointedPerson), "Role", DisplayName = "Role Appointment")]
public class Person : FullAuditedEntity<Guid>, IMayHaveTenant

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class DisplayManyToOneAuditTrailAttribute : Attribute
{
public Type ManyToOneEntityType { get; set; }
public string DisplayName { get; set; }
public string RelatedEntityField { get; set; }
public string NameField { get; set; }
public string[] AuditedFields { get; set; }

public DisplayManyToOneAuditTrailAttribute(Type manyToOneEntityType)
}

Example:

[DisplayManyToOneAuditTrail(typeof(DepartmentUser), DisplayName = "School user")]
public class School : Facility { }

public class DepartmentUser : GDEPerson
{
[Audited]
public virtual School School { get; set; }
}

Shows events from related DepartmentUser entities as School user changes, e.g. "School user" added: <Full Name of department user>.


Ending the Audit Trail at a Specific Value

Stop showing further audit records once a property reaches a given value.

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class PropertyChangeToStopAuditTrailAttribute : Attribute
{
public string PropertyName { get; set; }
public string PropertyValue { get; set; }

public PropertyChangeToStopAuditTrailAttribute(string propertyName, string propertyValue)
}

Example:

[PropertyChangeToStopAuditTrail("SchoolVerificationOutcome", "Deleted By Parent")]
public class SchoolApplication : FullAuditedEntity<Guid>

Once SchoolVerificationOutcome changes to Deleted By Parent, any audit records added after that point are not displayed for this entity.


Adding History Events Directly Via SQL

If you make direct database changes outside the application, you can add matching entity history events with stored procedures instead.

Adding a List of Changes

exec [dbo].[Core_AddEntityHistoryEvents] @changeTime, @reason, @tenantId, @userId, @changes
ParameterDescription
@changeTimeDate and time of the changes. Null is replaced with the current time
@reasonReason for the changes
@tenantIdTenant ID
@userIdUser ID
@changesA table-valued parameter of type Core_EntityHistoryItem listing the changes
CREATE TYPE Core_EntityHistoryItem AS TABLE
(
ChangeType tinyint, -- 0 = Created, 1 = Updated, 2 = Deleted
EntityId nvarchar(48),
EntityTypeFullName nvarchar(192), -- fully qualified name, namespace included, assembly excluded

PropertyName nvarchar(96),
PropertyTypeFullName nvarchar(256),
NewValue nvarchar(512),
OldValue nvarchar(512),

Description nvarchar(512) -- optional
)

Example - unlock all locked users, with a property change recorded:

Declare @changes Core_EntityHistoryItem

insert into @changes
select 1 /* Updated */, Id, 'Shesha.Authorization.Users.User', 'IsLocked', 'System.Boolean', 'false', 'true', 'User unlocked'
from Core_Persons
where IsLocked = 1

exec [dbo].[Core_AddEntityHistoryEvents] null /* current time */, 'Support ticket #12345: Unlock all users', null, 1 /* Admin user ID */, @changes

This shows as a User updated event with the description 'IsLocked' was changed from 'true' to 'false' (or, since a description was supplied, User unlocked).

Example - the same change without property data, recorded as a plain event:

Declare @changes Core_EntityHistoryItem

insert into @changes
select 1 /* Updated */, Id, 'Shesha.Authorization.Users.User', null, null, null, null, 'User unlocked'
from Core_Persons
where IsLocked = 1

exec [dbo].[Core_AddEntityHistoryEvents] null, 'Support ticket #12345: Unlock all users', null, 1, @changes

This shows as a User unlocked event with no property-change detail.


Adding a Single Change

exec [dbo].[Core_AddSingleEntityHistoryEvent] @changeTime, @reason, @tenantId, @userId, @changeType, @entityId, @entityTypeFullName, @propertyName, @propertyTypeFullName, @newValue, @oldValue, @description
ParameterDescription
@changeTimeDate and time of the change
@reasonReason for the change
@tenantIdTenant ID
@userIdUser ID
@changeType0 = Created, 1 = Updated, 2 = Deleted
@entityIdID of the changed entity
@entityTypeFullNameFully qualified entity type name (namespace included, assembly excluded)
@propertyNameName of the changed property
@propertyTypeFullNameFully qualified property type name
@newValueNew value
@oldValueOld value
@descriptionOptional description

Example - unlock a specific user, with property change data:

exec [dbo].[Core_AddSingleEntityHistoryEvent] null, 'Support ticket #12345: Unlock all users', null, 1, 1, 123456, 'Shesha.Authorization.Users.User', 'IsLocked', 'System.Boolean', 'false', 'true', 'User unlocked'

Example - the same change without property data:

exec [dbo].[Core_AddSingleEntityHistoryEvent] null, 'Support ticket #12345: Unlock all users', null, 1, 1, 123456, 'Shesha.Authorization.Users.User', null, null, null, null, 'User unlocked'