Download the sample code for this post.
In part one of this three part series, I offered what we’d ideally like when using NHibernate, or any ORM for that matter, with custom collections in the domain layer. To reiterate, the ORM should have as little impact as possible on how custom collections within the domain are structured and how relationships between classes are declared. At the very least, there should be no references to the ORM, or ORM-supporting assemblies, such as the deprecated NHibernate.Generics, from the domain, whatsoever.
These goals are a bit vague, so let’s make them a bit more measurable:
We should be able to create a custom collection which does not depend on data layer references.
The custom collection’s container should be able to hold a pointer to the custom collection without having to go through a wrapper class or data layer intermediary.
The entire domain layer should contain no assembly references to data layer dependencies.
And, obviously, NHibernate’s behavior should not be adversely affected by these restrictions.
Well, that sounds simple enough. The example solution, using NHibernate 1.2.1, shows the bare minimums for achieving these goals. To get the sample working locally, simply modify the connection string within /CustomCollectionsBasic.Web/bin/web.config to point to your Northwind database. (Keep in mind that this is an illustrative sample so there'll be plenty to nitpick in the presentation layer.)
The CustomCollectionsBasic.Core Class Library
The first three goals concerning keeping the domain layer clean are a good place to start, so let's take a look at the domain layer which is in the CustomCollectionsBasic.Core class library. This class library maps two classes, Category.cs and Product.cs, to their respective tables in Northwind and has a one-to-many relationship from a category to zero or more products. Our motivating factor for wanting to create a custom collection, in this example anyway, is to be able to sort the products associated with a category. (There are better reasons to have a custom collection...but this'll do until I find some creativity.)
Let's take a closer look at Category.cs. You'll note that it inherits from DomainObject which enables domain objects to be compared conveniently and provides objects with their ID property to be used for persistence. Obviously, this exists due to data layer requirements, but it's never given me grief like collection management has. Note also that this inheritance doesn't have anything to do with custom collections; it doesn't implement anything from a data layer to enable custom collections. The virtual keywords exist to allow lazy loading of the object; these are optional and could be removed altogether if you're OK with the object not being lazily loaded. But again, having the virtual keywords don't muddle the waters much and aren't going to create a likely area for the introduction of bugs. GetHashCode is an implementation detail of DomainObject; you can read about the DomainObject class and all its gory details in a previous post.
Now, the interesting part (or I suppose uninteresting is our point here) is the collection of products that the category class holds. The
ProductsInCategory property has a POCO getter and setter, exposing an
IProducts interface. Working via an interface for the collection is the only effect of having to accommodate the ORM, and it's very unobtrusive as the interface simply implements
IList<Product>. Furthermore, you'll note that the collection's member field,
productsInCategory, has been initialized as a new POCO products collection and it's setter is
protected so you never have to worry about it being null when dealing with
The following diagram illustrates the gist of the domain model:
As the diagram shows, the Category.cs class points to a collection, the
IProducts interface. The Products.cs collection simply implements that interface. And that's it. The domain is very clean with respect to managing the custom collection...no wrapper class and no ComplicatedPersistentListBaseClassComplicatingItAll.
So that knocks off the first two goals concerning the creation of a clean, custom collection and the relationship to it from a container class. And to verify the the third goal, concerning having no data-layer, assembly references in the domain, simply take a look under CustomCollectionsBasic.Core/References. Besides the HBMs, the only other thing in this class library is the Design-by-Contract utility written by Kevin McFarlane.
Obviously, the HBMs are data-centric and break up the data-concern-aversion-ness (is that a word?) of the domain layer. But having them in the domain is strictly for convenience and ease of maintenance. They could just as easily be put into the CustomCollectionsBasic.Data class library...which brings us to our next area of discussion... (We'll come back to the HBMs in just a moment.)
The CustomCollectionsBasic.Data Class Library
This library holds all the ugly stuff for managing the NHibernate session (/NHibernate/NHibernateSessionManager.cs), setting up the generic DAO (/NHibernate/NHibernateDao.cs) and concrete DAO for the Category class (/Daos/CatgoryDao.cs), and, finally, and most importantly, holds the custom collection implementation details that NHibernate needs in order to work through the
IProducts interface in the domain layer. (For a discussion of the NHibernateSessionManager class, you can read my NHibernate article on codeproject.com.)
In order for you to create custom collections that NHibernate can work with, you need two classes:
A Persistent Collection (/Collections/PersistentProducts.cs): The persistent collection is the custom collection in "NHibernate speak" and is what NHibernate is capable of working with. The simplest way to create your own is to simply inherit from NHibernate's PersistentGenericBag or a similar such collection. Looking at the code, you'll note that the PersistentProducts.cs class also implements the
interface that we defined in the domain layer. This is an example of a Separated Interface (or Dependency Inversion)
and provides a means for creating a clean separation of concerns between the domain and the data layer.
A Persistent Collection Type (/Collections/PersistentProductsType.cs): This is the type declaration of the collection so that NHibernate can interpret an HBM mapping and translate the results from the database into the custom collection, accordingly. The type implements NHibernate's not-very-fun-to-implement
Now, to make this all work, a mapping has to be provided in Category.hbm.xml, back in the domain layer, that instructs NHibernate how to wire it all up. The example code reflects:
<bag name="ProductsInCategory" table="Products" cascade="all" inverse="true"
<one-to-many class="CustomCollectionsBasic.Core.Product, CustomCollectionsBasic.Core">
That's it for the data layer and wiring it all together. Running the Default.aspx page will show it all in action with lazy loading and all.
In the other NHibernate custom collection samples I've found, there's a lot more code than what I have in mine. I'd love for anyone to let me know if I've missed something here; but I don't think I have. This solution supports lazy loading and I've been using it in a variety of parent/child scenarios without any trouble.
So Who Needs a Part III?
This sample does a good job of meeting our goals by keeping the domain layer clean of fragile collection wrappers and/or data-layer references, but the magic that makes it happen isn't very reusable. Take a look at /Collections/PersistentProductsType.cs. Because of its direct dependence on /Collections/PersistentProducts.cs, you'll have to create an almost identical type class for each and every collection that comes along. Good luck changing them all if you find a bug or have a change to make for some reason. In the next part, I'll show how we can make the type class generic without having to create a complicated class hierarchy. The downside is that our HBM collection mapping is going to get a bit uglier...er...more fun. What, you thought nothing would have to give? ;) We'll also see a few of those data-oriented classes pulled out into a reusable library.
Until then, happy enumerating.
12-06-2007 9:58 PM