In part I of this series, we examined motivations for maintaining custom collections that are compliant with NHibernate. Part II demonstrated the basics behind wiring up a custom collection. Finally, part III refactored the solution into a nice reusable package with minimal intrusion (relatively speaking). My complaint with all of this is that the proposed solution is complicated, difficult to explain, and leans towards Shotgun Surgery by requiring you to change the signature of both the POCO collection as well as the NHibernate collection every time you need to add a new method. That's just not cool. As much bad press that extension methods have received, I have found one instance (and only one so far) when extension methods truly shine: extending collection interfaces, which NHibernate can happily bind to, with custom methods.
Ideally, we want our custom collection to be completely ignorant that it's being bound to by an ORM. In the previous suggestion (part III), we ended up with an IProducts interface to define the custom collection, a Products class to implement the custom methods, and a PersistentProduct class for NHibernate to bind to. (We also had a very ugly HBM mapping, but I digress.) This all seems a bit overkill and more than a stone's throw from being completely ignorant of the data-access mechanism. For this demonstration, assume that a Customer has one or more orders; furthermore, we'd like to be able to add custom query methods onto the orders collection. But since NHibernate has to bind to an interface, let's start there within our Customer class:
/// <summary>
/// Assuming we want to leverage automatic properties,
/// init the collection here.
/// </summary>
public Customer() {
Orders = new List<Order>();
}
/// <summary>
/// Note the protected set; only the ORM should set the collection
/// reference directly after it's been initialized in the constructor.
/// </summary>
public IList<Order> Orders { get; protected set; }
Now that we're just binding to a collection interface, the mapping becomes trivially simple:
<bag name="Orders" inverse="true">
<key column="CustomerID">
<one-to-many class="Order">
</bag>
Are you starting to get a sense for how much simpler this is going to be? So we've got NHibernate binding to the collection, but we still need to provide custom methods on the collection; that's where extension methods come in! Suppose that you have a need to regularly query the collection for all the orders that fall on a particular date. As we concluded in part I of this series, we'd ideally like this mechanism to be on the collection itself. By adding an extension method to the IList<Order> interface, our task is complete:
public static class OrdersExtensions
{
/// <summary>
/// Extends IList<Order> with other,
/// customer-specific collection methods.
/// </summary>
public static List<Order> FindOrdersPlacedOn(this IList<Order> orders,
DateTime whenPlaced)
{
return (
from order in orders
where order.OrderDate == whenPlaced
orderby order.OrderDate
select order)
.ToList<Order>();
}
}
With the extension method in place, we now have full use of the method from an IList<Order> collection with intellisense.
So where's the rub? The drawback to this is that extension methods are a bit evil magical and can be difficult to notice if they're not kept well organized. Accordingly, follow these guidelines when using this approach to make it easier for the next developer (usually yourself) who looks at your code:
- Name the file (and class) which contains the extensions as <domain object plural>Extensions.cs; e.g., OrdersExtensions.cs, CustomersExtensions.cs. This is similar to what we were doing before, e.g. Products, but tacks on the word "Extensions" to make it clear that these are extensions and not an actual collection object.
- Put the extensions class physically next to the domain object which it works with. For example, if the Order.cs class file sits in the directory MyProject.Core/Checkout, then OrdersExtensions should sit right next to it. This makes it easier to spot when browsing the project files.
- Keep each collection of extension methods logically contained in individual files, one for each domain object they work with. Doing so emulates what we would have done if we were using "traditional" custom collection classes where we would have had e.g. Orders.cs and Products.cs. Contrastly, you shouldn't put every extension method into a ginormous MyCollectionExtensions.cs class. This breaks the SRP, makes it difficult to digest when it gets larger, and can quickly become a maintenance headache nightmare.
I've been using this approach for the creation of custom collection behavior for the past six months or so and have been very pleased with the ease of use and extensibility. They're also leveraged in the S#arp Architecture example code. While extension methods may be a bit on the evil side, they make the challenge of using custom collections with NHibernate terribly trivial. >-)
Billy McCafferty
Posted
09-03-2008 4:10 PM
by
Billy McCafferty