Sometimes the basic information provided by the default revision entity we have in NHibernate.Envers are not enough, when we need to extend those information and provide additional data we have the option to use a customized version of the Revision Entity class.

The default revision entity is defined like this:

[Serializable]
public class DefaultRevisionEntity
{
	[RevisionNumber]
	public virtual int Id { get; set; }

	[RevisionTimestamp]
	public virtual DateTime RevisionDate { get; set; }

	public override bool Equals(object obj)
	{
		if (this == obj) return true;
		var revisionEntity = obj as DefaultRevisionEntity;
		if (revisionEntity == null) return false;

		var that = revisionEntity;

		if (Id != that.Id) return false;
		return RevisionDate == that.RevisionDate;
	}

	public override int GetHashCode()
	{
		var result = Id;
		result = 31 * result + (int)(((ulong)RevisionDate.Ticks) ^ (((ulong)RevisionDate.Ticks) >> 32));
		return result;
	}
}

You can notice two properties that defines the information that a Revision Entity class MUST have, they are also marked by two configuration attributes:

  • [RevisionNumber] - states which property will represent the revision number of the version
  • [RevisionTimestamp] - states the timestamp of the version

Every custom Revision Entity class MUST have these two kind of properties, once again we will use the fluent by code configuration so we will not need to decorate our classes with attributes (I’ll show how to configure NHibernate.Envers using attributes in another post).

Our custom Revision Entity class for this example will be:

//[RevisionEntity]
public class REVINFO
{
	//[RevisionNumber]
	public virtual long Id { get; set; }

	//[RevisionTimestamp]
	public virtual DateTime CustomTimestamp { get; set; }

	public virtual string Data { get; set; }

	public override bool Equals(object obj)
	{
		var casted = obj as REVINFO;
		if (casted == null)
			return false;
		return (Id == casted.Id &&
				CustomTimestamp.Equals(casted.CustomTimestamp) &&
				Data.Equals(casted.Data));
	}

	public override int GetHashCode()
	{
		return Id.GetHashCode() ^ CustomTimestamp.GetHashCode() ^ Data.GetHashCode();
	}
}

As you can see we have the two basic properties plus an additional Data field that will represent our custom data added to each revision tracked.

To be able to fill these data we need to implement a particular type of interface: an IRevisionListener. This interface expose only a single member function - void NewRevision(object) - and it’s called every time a new revision object is generated. The demo code is:

public class RevInfoListener : IRevisionListener
{
	public static string Data = "test data";

	public void NewRevision(object revisionEntity)
	{
		((REVINFO)revisionEntity).Data = Data;
	}
}

The last step is to change the configuration to ‘inject’ our custom Revision Entity class and Listener; in my test project I do it with the following code:

public class NHinitCustomRevInfo : NHibernateInitializer
{
	protected override System.Collections.Generic.IEnumerable<System.Type> GetDomainEntities()
	{
		// I am using ConfORM to emit the mappings, so we add it to the mapped classes.
		return base.GetDomainEntities().Union(new[] { typeof(REVINFO) });
	}
	
	protected override void ConfOrmMapping(ConfOrm.ObjectRelationalMapper orm, ConfOrm.NH.Mapper mapper)
	{
		// I have to provide a mapping for a custom RevInfo class...NHibernate must know how to handle these objects
		orm.TablePerClass<REVINFO>();
		
		orm.TablePerClass<Person>();
		orm.TablePerClass<Game>();
	}

	public void InitializeAudit()
	{
		// initialize the NHibernate.Envers fluent configuration object
		var enversConf = new NHibernate.Envers.Configuration.Fluent.FluentConfiguration();

		// I prefer to not use attributes to configure the custom revision entity
		enversConf.SetRevisionEntity<REVINFO>(e => e.Id, e => e.CustomTimestamp, typeof(RevInfoListener));
		
		// the RevInfo class must not be in the auditing list
		enversConf.Audit(GetDomainEntities().Where(e => !typeof(REVINFO).IsAssignableFrom(e)));

		// to inspect the metadata
		//var mets = enversConf.CreateMetaData(Configure);

		// Configure.Properties.Add("nhibernate.envers.audit_table_prefix", string.Empty); // default
		Configure.Properties.Add("nhibernate.envers.audit_table_suffix", "_REV"); // default _AUD
		// Configure.Properties.Add("nhibernate.envers.revision_field_name", "REV"); // default
		// Configure.Properties.Add("nhibernate.envers.revision_type_field_name", "REVTYPE"); // default

		Configure.IntegrateWithEnvers(enversConf);
	}
}

I am using ConfORM and my mapping engine, so part of the configuration is related to how it’s used to generate the mappings, but the concepts are valid for any other mapping tools you use; Let’s go through it:

  • Line 6: we add the REVINFO class to my mapped domain, ConfORM need to be aware of it.
  • Line 12: we tell ConfORM how to map the class.
  • Line 21: we initialize the NHibernate.Envers fluent configuration engine.
  • Line 24: this is the crucial point, we specify our custom Revision Entity class type, we tell Envers which functions to use to fill in the Revision incremental number and the Revision Timestamp; plus we pass in the type of the IRevisionListener implementation if we want to specify additional data in the custom revision entity class.
  • Line 27: we tell to NHibernate.Envers which classes we want to track (be careful to exclude the REVINFO class from the tracked ones here).
  • Line 37: we add NHibernate.Envers to the standard NHibernate configuration.

If we create a simple test and execute it we can inspect the schema of the generated database to see the custom revision entity in place:

Database Schema with Custom Revision Entity

Figure 1 - Database Schema with Custom Revision Entity

That’s all for this post, next time we’ll see how to query for the extended revision entity properties we just added.

Related Content