Some business systems use simple change tracking. User name and timestamp are saved when object is created and modified. In this posting I will show how to do it easily with NHibernate.
The trick is to use interceptor class and plug it to NHibernate when NHibernate session is configured.
Trackable entities
As a first thing we must guarantee that our entities are trackable and we need generalization for this. If code uses base class for entities and all entities are tracked then we can use base class like this:
public abstract class BaseEntity
{
public virtual int Id { get; set; }
public virtual DateTime Modified { get; set; }
public virtual string ModifiedBy { get; set; }
public virtual DateTime Created { get; set; }
public virtual string CreatedBy { get; set; }
}
If only some mapped objects in code are trackable then we have to use another base class between base entity and trackable entity. Also we can go with interface and define ITrackable interface. Base class seems better to me as we don’t have to define tracking fields again in classes.
This generalization gives us one more advantage in big picture: when adding new trackable entity to system we have to inherit it from trackable base class. After this tracking works automagically.
Tracking interceptor
NHibernate allows us to define interceptors. Interceptors are plug-ins that provide external functionality for enitites that are about to change some way.
NHibernate provides EmptyInterceptor as base class for other interceptors. It defines dummy methods to override and this way we don’t have to use IInterceptor interface which means implementing all possible tracking methods.
Here is my (simple?) tracking interceptor:
public class TrackingInterceptor : EmptyInterceptor
{
public override bool OnSave(object entity, object id, object[] state,
string[] propertyNames,
global::NHibernate.Type.IType[] types)
{
var time = DateTime.Now;
var userName = // Find user name here
var typedEntity = (BaseEntity)entity;
typedEntity.Created = time;
typedEntity.CreatedBy = userName;
typedEntity.Modified = time;
typedEntity.ModifiedBy = userName;
var indexOfCreated = GetIndex(propertyNames, "Created");
var indexOfCreatedBy = GetIndex(propertyNames, "CreatedBy");
var indexOfModified = GetIndex(propertyNames, "Modified");
var indexOfModifiedBy = GetIndex(propertyNames, "ModifiedBy");
state[indexOfCreated] = time;
state[indexOfCreatedBy] = userName;
state[indexOfModified] = time;
state[indexOfModifiedBy] = userName;
return base.OnSave(entity, id, state, propertyNames, types);
}
public override bool OnFlushDirty(object entity, object id, object[] currentState,
object[] previousState, string[] propertyNames,
global::NHibernate.Type.IType[] types)
{
var userName = // Find user name here
var time = DateTime.Now;
var indexOfCreated = GetIndex(propertyNames, "Created");
var indexOfCreatedBy = GetIndex(propertyNames, "CreatedBy");
var indexOfModified = GetIndex(propertyNames, "Modified");
var indexOfModifiedBy = GetIndex(propertyNames, "ModifiedBy");
var typedEntity = (BaseEntity)entity;
if (typedEntity.Created == DateTime.MinValue)
{
currentState[indexOfCreated] = time;
currentState[indexOfCreatedBy] = userName;
typedEntity.Created = time;
typedEntity.CreatedBy = userName;
}
currentState[indexOfModified] = time;
currentState[indexOfModifiedBy] = userName;
typedEntity.Modified = time;
typedEntity.ModifiedBy = userName;
return base.OnFlushDirty(entity, id, currentState, previousState, propertyNames, types);
}
private int GetIndex(object[] array, string property)
{
for (var i = 0; i < array.Length; i++)
if (array[i].ToString() == property)
return i;
return -1;
}
}
For new entities all tracking fields are valuated. For modified enitities only modification related fields are valuated. This is all we need to make entities trackable.
NB! If youhave also non-trackable objects then make sure control in the beginning of overriden methods if given entity is trackable or not.
Plugging interceptor to session
And here is how to plug it to NHibernate session:
private ISession GetNewSession()
{
if (_currentSession == null)
{
_currentSession = _sessionFactory.OpenSession(new TrackingInterceptor());
}
return _currentSession;
}
Conclusion
We used NHibernate interceptor to make enitities trackable. Interceptor was built by extending EmptyInterceptor and overriding methods we need for entity tracking. Also it was easy to plug our interceptor to NHibernate session. After getting tracking work we don’t have to return back there – all new trackable classes will be tracked automatically.
View Comments (2)
I have used IPreUpdateEventListener and IPreInsertEventListener for the same purposes and registered them in NHibernate configuration section. Any specific reason why you use interceptors?
Also, when defining interface like ITrackable, it's more convenient to encapsulate these four properties in it's own class (ie. TrackingInfo).
If you're using some kind of NHibernate facility (eg Transaction facility, as we are) you may not have access to the SessionFactory OpenSession() method to set the interceptor as in the example here... So we set the interceptor via NHibernate's fluent configuration like this:
return new NHibernate.Cfg.Configuration().DataBaseIntegration(db =>
{
db.ConnectionString = "aConnectionString";
db.Dialect();
db.IsolationLevel = IsolationLevel.ReadCommitted;
})
.SetInterceptor(new ObjectInterceptor())
...