This series of posts is about overcoming a restriction, that O/R mappers like NHibernate have with respect to lazy loading and polymorphic type information. (Please refer to the problem description and example in part 1 and 1.5.) The previous part of this series demonstrated how we can fetch type-discriminating data from the db during the regular insert/update/retrieve lifecycle of an instance, along with its 'normal' data, and totally transparent from the domain perspective. This part now will show how we can make use of this data to make NHibernate generate a strongly-typed lazy-loading proxy - instead of a polymorphic one, which can lead to some pretty sophisticated complications, as the examples demonstrate.
So where are we? Currently, we have an 'entity' (NH's notion of an in-memory object that relates to a certain domain object) with a 'hidden' type discriminator value. Now we need to exploit that value to make NH generate a dynamic proxy of the desired type.
To do that, we first need to know something about the extension points that NH provides for hooking into the object creation process. Basically, there are two different mechanisms, namely Interceptors and Events, as this knowledge article describes in some more detail. Additionally, we have to keep in mind that NH entities are undergoing a two-phased loading process:
- During the first step, the raw instance data are fetched from the db - including possible reference data like the BOOKS.AUTHOR column in our example, which is a foreign key reference to a row in the AUTHORS table.
- In a second pass, these relational data will then be converted to their actual object references which will finally surface on the domain model. During that step, NH creates dynamic proxies as needed. (Well, this is by far not the only thing that's happening in the second phase, it's just the here relevant piece. If you'd like to get a deeper view on that and on how these operations may influence NH's performance, I recommend you this post. - Many of the complaints about NH's performance come from improper understanding of this step!)
Hooking into NH's loading process
To interfere with the proxy creation process, we will listen to NH's LoadEvent event. The standard DefaultLoadEventListener class is NH's implementation of the loading process and is invoked without any configuration. It is a pretty complex beast that coordinates the entire loading phase. Among many other things, it calls itself recursively for proxy creation with the 'InternalLoadLazy' load type. This is where we need to step into the process with the below:
public class PolymorphicResolvingLoadListener : ILoadEventListener
{
public void OnLoad(LoadEvent @event, LoadType loadType)
{
if (loadType == LoadEventListener.InternalLoadLazy)
{
IPersistenceContext persistenceContext = @event.Session.PersistenceContext;
// get the entity that is currently undergoing the two-phase load process
EntityEntry loadingEntityEntry = persistenceContext.GetLoadingEntityEntry();
if (loadingEntityEntry != null)
{
IEntityPersister loadingEntityPersister = loadingEntityEntry.Persister;
for (int i = 0; i < loadingEntityPersister.PropertyNames.Length; i++)
{
EntityType type = loadingEntityPersister.PropertyTypes[i] as EntityType;
if (type == null || type.Name != @event.EntityClassName)
{
continue; // this is not an entity at all or an entity of
} // the 'wrong' type, so skip it
// create the expected name for the discriminator property
string discriminatorPropertyName =
string.Format("{0}__discriminator__", loadingEntityPersister.PropertyNames[i]);
// get the currently loaded value - can be an ID or an INHibernateProxy instance
object loadedValue =
loadingEntityEntry.GetLoadedValue(loadingEntityPersister.PropertyNames[i]);
if (!(loadingEntityPersister.PropertyNames.Contains(discriminatorPropertyName)) ||
loadedValue is INHibernateProxy)
{
continue; // this property has no related discriminator property,
} // or it has already a proxy assigned, so skip it
// now get the actual discriminator value...
object discriminatorValue =
loadingEntityEntry.GetLoadedValue(discriminatorPropertyName);
// ...and replace the entity class name accordingly. NH will create
// a dynamic proxy for the here provided type.
@event.EntityClassName =
InheritanceMap.GetSubclassFor(type.Name, discriminatorValue);
break;
}
}
}
}
} // class PolymorphicResolvingLoadListener
As you can see, we don't have to soil our hands with creating a proxy ourselves. Rather we simply tell NH what exact class we want a proxy for, and NH will do all the rest for us. To make this happen, we only need to change the value of the EntityClassName property of the LoadEvent argument accordingly, after we have accessed and evaluated our 'hidden' type discriminator value.
To finally put our custom event listener into action, we need a configuration entry telling NH to invoke it - in addition to the NH default listener:
<session-factory>
...
<event type="load">
<listener class="LLDemo.Persistence.PolymorphicResolvingLoadListener, LLDemo.Persistence"/>
<listener class="NHibernate.Event.Default.DefaultLoadEventListener, NHibernate"/>
</event>
</session-factory>
Alternatively, you can do this programmatically:
ILoadEventListener[] listenerStack = new ILoadEventListener[]
{
new PolymorphicResolvingLoadListener(),
new DefaultLoadEventListener()
};
configuration.EventListeners.LoadEventListeners = listenerStack;
Exploiting the configuration metadata
You may have noticed the invocation of the InheritanceMap class at the end of the above shown OnLoad() method. This helper is used to find the correct subclass for a given superclass/discriminator pair. It is initialized during the application's initialization phase, together with NHibernate's own initialization. The respective call looks like this:
var configuration = new Configuration().Configure();
InheritanceMap.Initialize(configuration);
During initialization, NHibernate creates a very rich meta-level model of the mapped domain and all the various details that are needed to do the actual mapping later on. The InheritanceMap class evaluates these data and extracts the relevant information from it. A possible implementation of the Initialize() method would be:
public static void Initialize(Configuration configuration)
{
if (configuration == null)
{
throw new ArgumentNullException("configuration");
}
map = new Dictionary<string, string>();
PersistentClass[] subclasses = configuration.ClassMappings
.Where((@class) => @class.IsPolymorphic &&
@class.Superclass != null &&
!string.IsNullOrEmpty(@class.DiscriminatorValue))
.Cast<PersistentClass>()
.ToArray();
foreach(var subclass in subclasses)
{
map.Add(GetKeyFor(subclass.Superclass.EntityName,
subclass.DiscriminatorValue),
subclass.EntityName);
}
isInitialized = true;
}
The provided demo solution
To see all this in more detail, please refer to the provided sample solution (VS 2008). It includes not only the full source code, but also a db script to create the necessary sample data. The script and the NH configuration settings assume that the code is running against a MS SQL server instance - you will need to adjust the connection string in the App.config file to target your specific environment settings.
Furthermore, I should make the disclaimer that this sample is not intended to be a full, all-purpose solution for the here discussed issues. For example, it doesn't take into account multi-level inheritance hierarchies, and it also doesn't handle collections.
In addition to the here described event listening and configuration code, the solution also makes use of NH's new capability to invoke an IoC container for object/proxy creation (I used the Castle/Windsor container here). This overcomes another restriction that you usually buy with NHibernate: Namely that you have to provide a default constructor for your domain objects and that you have to make almost everything virtual. While that may go almost unconsciously for experienced NH users, here again the persistence system dictates how to write domain code, ultimately undermining the concept of Persistence ignorance. But that's left to the next part...