NHibernate is an extremely powerful ORM but sometimes its quite difficult to find information on how to do certain things and why other things are the way they are. Recently I've been working on generating NHibernate mapping files and a domain model with its classes using Visual Studio DSL tools. Below are some of the difficulties I came across and their solutions.
Mapping Enums to string database representations
Sometimes you have a database schema which requires you to provide things like "M" for male and "F" for female. It does not make sense to force your users to use a string for the gender. We simply want an enum. On the other hand we cannot simply put an integer in the database as it then requires the domain to have any context and is not self-describing. What we really want is enums in our domain and single char fields in our db.
Obviously we need some custom mapping. This is where the IUserType interface from NHibernate comes into play. You can implement this type on a converter class and specify the converter type in the type attribute in your NHibernate mapping instead of the actual enum type.
Generic Identifiers and Generators
Sometimes you have a mix of identifier types in your database schema, we may have guids, ints and even strings (eh? strings? what were you thinking?). You could implement a generic identifier type which hides these three types as you never really care about the content of the identifier, only that the ORM can use it to query etc. The problem is how do we tell NHibernate to convert this type and how do we allow a generator to build the e.g. guid before sending to the database?
First, your identifier type needs to implement a Identifier.Empty as default and allow it to always return default(T) as a non-instantiated version when you new-up an object in your domain. NHibernate will detect this equality and differentiate between new instances and instances which need to be updated instead of inserted.
Secondly, you need to implement an IUserType as above for each schema type. This will be the type specified in your HBM file and will convert between db type and your all-encompassing Identifier type.
Now we have a problem, the standard id generators in NHibernate no longer work as they want to generate the new id in the domain class before persisting, this will cause a type cast failure between the expected db type output from the generator and your custom identifier type. This time you need to create a custom identifier using a new class which implements NHibernate.Id.IPersistentIdentifierGenerator and NHibernate.Id.IConfigurable. This can detect the type of the real db schema and generate the correct type. You can also pass params to allow this to be configurable.
ISet<T> and IList<T> - sets, bags etc
NHibernate requires the usage of a set to allow the tracking of items added and removed on a collection. Unfortunately there is no concept of a set in the .Net Framework. The implementation of a set is provided in the Iesi.Collections dll which is shipped with NHibernate.
At runtime NHibernate tries to wrap the collection on a property which is defined in the HBM as a set with a collection which derives from Iesi.Collections.Generic.ISet<T> it can use to track changes in the collection. This allows NHibernate to only issue updates to changed members rather than deleting and recreating the entire collection. Normally the collection Iesi.Collections.Generic.HashedSet<T> is used in the domain class and this is then wrapped by NHibernate at runtime in a NHibernate.Collection.Generic.PersistentGenericSet<T> which allows the tracking of changes. This wrapping happens when a transient collection is saved or when a persisted collection is returned from the database.
This means that we cannot have purely CLR types in our domain model because we need to have an implementation of the ISet for transient and persisted collections. To protect the domain model in the possible future event of replacing NHibernate as a persistence strategy (and cos we don't want persistence creeping into our domain), new collection types have to be created. Create your own interface e.g. ISet<T>. This ISet<T> implements IList<T> and Iesi.Collections.Generic.ISet<T> allowing us to use standard IList<T> and ICollection<T> methods while keeping compatibility with NHibernate. The transient version of the set is called e.g. Set<T>, this also implements IUserCollectionType which defines the contract for NHibernate specific wrapping to persistent collection etc. The persisted version is called e.g. PersistentSet<T> and this inherits from NHibernate.Collection.Generic.PersistentGenericSet<T> to give NHibernate the change tracking it requires.
To use the new Sets in a domain, the following implementation is necessary.
- The private fields and public properties for collections are defined as IList<T>. This allows standard interaction with the collection for domain consumption.
- The actual implementation when the domain needs to instantiate a collection is of type MyNamespace.Set<T> to allow NHibernate to cast properly at runtime.
- In the case of readonly collections, one of the two readonly collections is created from the constructor passing in the IList<T> as part of the get accessor of the property
- In the HBM mapping file the collection-type attribute is added to the set declaration with the generic type name of the transient collection e.g. collection-type="MyNamespace.Set`1[[MyNamespace.MyDomain.MyDomainClass]]"
Once this is done you should be able to save and retrieve your collections without issues. Notice that when you save a transient collection, it is automatically cast to your persistent collection using the Wrap method of IUserCollectionType and this type is also used when retrieving from the db. Nice!