NHibernate & INotifyPropertyChanged

time to read 4 min | 630 words

One of the things that make NHibernate easy to use is that it fully support the POCO model. But one of the things that most people do not consider is that since NHibernate did the hard work of opening up the seams to allow external persistence concerns, we can use the same seams to handle similar infrastructure chores externally, without affecting the POCO-ness of our entities.

Allow me to show you what I mean. First, we create the following factory, which makes use of Castle Dynamic Proxy to weave in support for INotifyPropertyChanged in a seamless manner:

public static class DataBindingFactory
{
	private static readonly ProxyGenerator ProxyGenerator = new ProxyGenerator();

	public static T Create<T>()
	{
		return (T) Create(typeof (T));
	}

	public static object Create(Type type)
	{
		return ProxyGenerator.CreateClassProxy(type, new[]
		{
			typeof (INotifyPropertyChanged),
			typeof (IMarkerInterface)
		}, new NotifyPropertyChangedInterceptor(type.FullName));
	}

	public interface IMarkerInterface
	{
		string TypeName { get; }
	}

	public class NotifyPropertyChangedInterceptor : IInterceptor
	{
		private readonly string typeName;
		private PropertyChangedEventHandler subscribers = delegate { };

		public NotifyPropertyChangedInterceptor(string typeName)
		{
			this.typeName = typeName;
		}

		public void Intercept(IInvocation invocation)
		{
			if(invocation.Method.DeclaringType == typeof(IMarkerInterface))
			{
				invocation.ReturnValue = typeName;
				return;
			}
			if (invocation.Method.DeclaringType == typeof(INotifyPropertyChanged))
			{
				var propertyChangedEventHandler = (PropertyChangedEventHandler)invocation.Arguments[0];
				if (invocation.Method.Name.StartsWith("add_"))
				{
					subscribers += propertyChangedEventHandler;
				}
				else
				{
					subscribers -= propertyChangedEventHandler;
				}
				return;
			}
			
			invocation.Proceed();

			if (invocation.Method.Name.StartsWith("set_"))
			{
				var propertyName = invocation.Method.Name.Substring(4);
				subscribers(invocation.InvocationTarget, new PropertyChangedEventArgs(propertyName));
			}
		}
	}
}

Now that we have this, we can start creating entities that support INotifyPropertyChanged simply by calling:

var customer = DataBindingFactory.Create<Customer>();

This customer instance supports change notifications, but it is not something that we had to do, and it is something that we can pick & choose. If we want to use the same entities in a different context, where we don’t need INPC, we can simply skip using the factory, and not deal with it at all.

Now, using the data binding factory is good when we create the instances, but how are we going to teach NHibernate that it should use the factory when creating entities? That is actually quite easy, all we need to do is write an interceptor:

public class DataBindingIntercepter : EmptyInterceptor
{
	public ISessionFactory SessionFactory { set; get; }

	public override object Instantiate(string clazz, EntityMode entityMode, object id)
	{
		if(entityMode == EntityMode.Poco)
		{
			Type type = Type.GetType(clazz);
			if (type != null)
			{
				var instance= DataBindingFactory.Create(type);
				SessionFactory.GetClassMetadata(clazz).SetIdentifier(instance,id, entityMode);
				return instance;
			}
		}
		return base.Instantiate(clazz, entityMode, id);
	}

	public override string GetEntityName(object entity)
	{
		var markerInterface = entity as DataBindingFactory.IMarkerInterface;
		if (markerInterface != null)
			return markerInterface.TypeName;
		return base.GetEntityName(entity);
	}
}

Note that this interceptor does two things, first, it handles instantiation of entities, second, it make sure to translate data binding entities to their real types. All we are left now is to set the interceptor and we are done. Again, this is an opt in option, if we don’t want it, we can just not register the proxy, and we don’t  worry about it.

Something that will probably come up is why not use NHibernate’s own proxy generation (byte code provider) to provide this facility. And the answer is that I don’t think it would be as easy as that. The byte code provider is there to provider persistence concerns, and trying to create a byte code provider that does both that and handle INPC issues is possible, but it would be more complicated.

This is a simple and quite elegant solution.