Audit Trailing – API Architecture for general purpose

  • by
Photo by Agence Olloweb on Unsplash

It is well-known that audit trails play a vital role in assisting businesses with maintaining the integrity of its systems and data. Whether it is logging a product design change, financial or communications transaction an audit trail validates actions and outcomes. For this reason, the ability to easily and seamlessly capture, persist and report audit information in a uniform and abstract fashion can be very beneficial.

In this post, we are going to look at a solution to audit changes to our data. When adding a new entity to our application, we would like to audit this entity with the same solution as we would audit any other database entity in our application. From the audit trail, we would like to see changes to selected fields on selected entities as well as who changed the values and when.

From the results above, the first record where Id = 54 shows the user, database entity, as well as when the changes where applied. The child records show the fields changed with the associated new value.

New changes to the specific entity (lead 108) will result in new audit records. The first set of records in the results above, where ID = 54, show the initial creation of the entity, followed by the last two records where ID = 56, showing the two fields FirstName and Age were updated. The audit trail will only record fields that have changed.

Two conditions are required to utilize the audit trail functionality. An implementation of the interface IAuditDefinition< TEntity > and the entity should be modified using a handler implementation of CommandHandler<TRequestDto, TEntity, TOut>.

public class LeadAuditDefinition : IAuditDefinition<Lead>
{
    public object GetAudit(Lead entity)
    {
        return new
        {
            LeadPersonalInformation = new
            {
                entity.LeadPersonalInformation?.FirstName,
                entity.LeadPersonalInformation?.Email,
                entity.LeadPersonalInformation?.Age,
                entity.LeadPersonalInformation?.Cellphone,
                entity.LeadPersonalInformation?.CurrentAdvisorId,
                entity.LeadPersonalInformation?.OccupationId,
            },
            entity.AgentId,
            AuthorizationGroups = new
            {
                AuthorizationGroupIds = entity.AuthorizationGroups
                    ?.Select(a => new {AuthorizationGroupId = a.AuthorizationGroupId})
                    .ToList()
            }
        };
    }
}

The LeadAuditDefinition implements a method GetAudit from the interface IAuditDefinition<T>. GetAudit returns an anonymous object representing the entity properties we want to audit as well as the structure we want to see in the audit trail Name field. The result is used to capture a snapshot of the entity’s state, as expressed in the GetAudit method, during the execution of the CommandWorkerHandler<>.

public class UpdateLeadWorkerHandler : CommandHandler<UpdateLeadRequest, Leadint>
{
    public UpdateLeadWorkerHandler(
        IMapper mapper,
        IRepository repository,
        ILifetimeScope scope,
        IWorkerContext<int> context)
        : base(mapper, repository, scope, context)
    {
    }

    protected override void GetEntity()
    {
        Entity = Repository.GetOne<Lead>(a => a.Id == Request.Id,
            "LeadPersonalInformation");
    }

    protected override void Execute()
    {
        Mapper.Map(Request, Entity);
        Repository.Save(Entity).SaveChanges();
        Context.Result = Entity.Id;
    }
}

The Audit Trail Plugin is accessible to the project through a nuget or an isolated project. Abstract handlers like the command handler above expose delegates before and after the two methods GetEntity and Execute. The Entity Audit Plugin hooks into these delegates to capture snapshots of the entity using the implementation LeadAuditDefinition.

When the UpdateLeadWorkerHandler executes, the plugin will recognize that the entity is auditable if an implementation of IAuditDefinition<Lead> is found. After completion of the GetEntity method, the plugin takes a snapshot of the Entity fetched from the repository. Changes are applied to the entity in the Execute method and persisted to the database. When completed the plugin takes a second snapshot of the entity. The two snapshots are then compared and the difference is written to an event and added to the Context.

The framework publishes all events collected throughout the execution pipeline cycle on completion. The process of transforming and persisting the audit trail to the database is offloaded to increase performance and user experience using an event-driven architecture.

On the roadmap, for this solution, we have:

  • Expose the Audit Plugin through an injectable interface to the handlers in order to reduce the dependency on TEntity and introduce more flexibility to the developer.
  • Introduce auditing for actions triggered and data accessed