Implement Audit Log for Legacy Code
Given a legacy project, the code has been developed for 6 years, the requirement is to log all the changes in the system. For example, if a user changes his street name, that must be logged so that the administrator can know the old and new values.
The project is a typical codebase where there is domain model, SQL Database using NHibernate framework.
The question is how do we implement that requirement without polluted the domain model, without making breaking changes in the design and functionalities?
Assuming that we have a super simple domain model with User and Address.
public class User { public Guid Id { get; set; } public string Name { get; set; } public ResidentAddress ResidentAddress { get; set; } public IList<VisitedAddress> VisitedAddresses { get; set; } } public class ResidentAddress { public string HouseNumber { get; set; } public string StreetName { get; set; } } public class VisitedAddress { public string HouseNumber { get; set; } public string StreetName { get; set; } public string Country { get; set; } } public class SuperCoolService { public void ChangeUserResidentStreet(User user, string streetName) { user.ResidentAddress.StreetName = streetName; } public void AddVisitedAddress(User user, VisitedAddress visitedAddress) { user.VisitedAddresses.Add(visitedAddress); } }
A user has a residential address and a number of visited addresses. Whenever a user changes his street name, audit log it. Whenever a user visits a new address, audit log it. You got the idea.
Capture Changes
We, unfortunately, cannot redesign the domain model. And we cannot go into every single method and detect if values change. No, that would pollute the domain model and destroy the design. Audit Log is a system concern, not a business domain concern.
It turned out that we can accomplish that goal with the beautiful design of NHibernate. In short, NHibernate allows us to hook in its event pipeline. It allows us to add custom logic for pre and post of Insert/Update/Delete an object. Check more detail from NHibernate document website.
When designing systems, I prefer the explicit approach instead of implicit. I want to have the power of deciding what objects I want to audit, at which properties. Life is so much easier if you are in the control. The same goes for code.
public interface IAuditable { } [AttributeUsage(AttributeTargets.Property)] public class ExplicitAuditAttribute : Attribute { } public class User : IAuditable { public Guid Id { get; set; } [ExplicitAudit] public string Name { get; set; } public ResidentAddress ResidentAddress { get; set; } public IList<VisitedAddress> VisitedAddresses { get; set; } } public class ResidentAddress : IAuditable { [ExplicitAudit] public string HouseNumber { get; set; } [ExplicitAudit] public string StreetName { get; set; } } public class VisitedAddress : IAuditable { [ExplicitAudit] public string HouseNumber { get; set; } [ExplicitAudit] public string StreetName { get; set; } [ExplicitAudit] public string Country { get; set; } }
All need-audit-log classes are decorated with the IAuditable interface. Auditable properties are marked with ExplicitAudit.
public class NhAuditUpdateDomainHandler : IPostUpdateEventListener { public void OnPostUpdate(PostUpdateEvent @event) { var auditable = @event.Entity as IAuditable; if (auditable == null) return; var dirtyFieldsIndex = @event.Persister.FindDirty(@event.State, @event.OldState, @event.Entity, @event.Session); if (dirtyFieldsIndex.Length == 0) return; // Custom logic to handle audit logic } }
Depending on your domain, your audit logic, the detail is yours. By implementing an IPostUpdateEventListener, we can extract all the [ExplicitAudit] properties with their old and new values. Pretty neat design.
Give It Meaning
Soon we hit a problem. We knew the street name was changed. However, we could not know to whom it belongs to. We cannot know the parent of the address. The current design only allows us to know the address of a user, not the other way around.
// Reality Street name change from "Batman" to "Superman" // However, we expect this User: Thai Anh Duc Street name change from "Batman" to "Superman"
Think about the situation where you have many changes. The information is useless.
Context. The address is lacking Context. At the time of audit log, it needs to know who it is a part of.
The question becomes how do we give that information for the Address class without introducing a hard reference back to the User class?
Explicit Tell
The goal is to minimize the impact on the existing codebase, to prevent changes in business logic. We cannot support the audit log feature without touching the existing code. I decided to categorize the domain objects into 2 categories: ProvideContext and DependableContext
public interface IAuditContextProvider { AuditContext ProvideContext(); } public interface IAuditDependableContext { AuditContext ProvidedContext(); }
- Context Provider: Will tell the infrastructure its own context. It acts like a root. It is the User class in our example.
- Dependable Context: Will tell the infrastructure the context it is provided. This allows the infrastructure links the context. It is the Address class in our example.
I must write some code to build the infrastructure to link them. It is infrastructure code, not interfere with the domain code.
There are 2 scenarios we have to deal with: Component and Reference relationship.
Component Relationship
In NHibernate with SQL, a component relationship means 2 objects are saved in the same table. Here is the detail document from NHibernate site.
Because address cannot stand alone. It cannot be added directly to NHibernate’s session. This gives me a chance to intercept its parent (the user object) when it is attached to NHibernate’s session. I just need to find a way to tell the NHibernate: Hey, whenever you save a user object (via its session), tell the user to populate the audit context to its components.
Thank the beautiful design of NHibernate, this code works beautifully
public class ProvideAuditContextSaveOrUpdateEventListener : ISaveOrUpdateEventListener { public void OnSaveOrUpdate(SaveOrUpdateEvent @event) { var mustProvideContext = @event.Entity as IMustProvideAuditContext; if (mustProvideContext != null) mustProvideContext.ProvideAuditContext(NHibernateUtil.IsInitialized); } }
What the code says is that “Hey, if you are a ‘must provide audit context (IMustProvideAuditContext interface)‘, then do it”. When the entity comes to the later phases in the pipeline, it has the audit context provided.
Reference Relationship
However, if the Address object is a reference object (in which it is saved in a separated table), it will not work. Because the referenced objects are inserted first. Take an example, we want to allow a user having many addresses. In this situation, User has its own table. And Address has its own table. There is a UserId column in the Address table.
Take an example, we want to allow a user having many addresses. In this situation, User has its own table. And Address has its own table. When a new address is added to a user, that address is saved first. Then the user is updated.
Because Address knows its user, we can take an advantage by overriding the ProvidedContext method
public override AuditContext ProvidedContext() { var context = base.ProvidedContext(); return context.ContextId == Guid.Empty ? new AuditContext{ContextId = UserId} : context; }
If it has a context, then use it. Otherwise, create a new one with UserId
Takeaway
When starting a new project, audit log might not be mentioned, might not be seen as a feature. We, as a developer, should ask customers, managers if we should concern about the Audit Log. No matter the answer, you should be aware of its coming. You should consider Audit Log when designing the code.
Audit Log is an infrastructure concern. Make sure you treat it as is. A very bad approach is to go in every single method call in the domain and register an audit log (or any kind of audit). There are better ways to deal with infrastructure concern from Domain code, depending on the framework you used. In my project, I accomplish that goal by combining
- Attribute: By decorated a property with [ExplicitAudit] attribute, I control what properties I want to audit.
- NHibernate Events: A well-designed framework will allow you to hook into its pipeline and add your custom logic. I take advantages of NHibernate well-designed events. If you use EF, I am sure you will find many places to hook your custom logic in.
Have you seen the power of OCP (Open-Closed Principle) in NHibernate design? Such a powerful principle.
The real implementation is far more complex than in this post. However, the principles stand. It was really a challenge.
So far so good. It is not a perfect implementation. But I am happy with the result.