Monday 27 October 2008

Searching, Ordering, and Paging with NHibernate

For a while now I've been looking at ways of making it as simple as possible to create scalable searches and add ordering and paging to them when using NHibernate as my ORM. I read this article by Oren a while back and it has guided my thinking since. I had the pleasure of mentioning this to Oren at the last ALT.NET drinks in London. He grabbed Seb's laptop and threw some code together, code very similar to this that has since appeared on his blog here.

I like the way that he has a class to organise the building of the criteria, it controls the complexity that filtering can quickly gain. I had a couple of reservations though. Primarily, the way that the filtering class assumes some of the responsibilities of the repository by executing the criteria. Relatedly, adding ordering and paging into the filtering class would seem to add additional responsibilities that do not belong to the class, but that I need to configure. So I've come up with a variant and with more than a little trepidation I'm putting it on my blog.

In the repository class I have a FindAll method that takes a DetachedCriteria object:

public IList<T> FindAll(DetachedCriteria criteria)
{
ICriteria toFire = criteria.GetExecutableCriteria(NHibernateHelper.OpenSession());
return toFire.List<T>();

}

To provide this DetachedCriteria I have a SearchCriteria object, in this case a CandidateSearchCriteria object. This is currently very simple in only providing for two fields but obviously more could be added to the interface and implemented. It provides a List of ICriterion objects.

public class CandidateSearchCriteria : List<ICriterion>, ICandidateSearchCriteria
{
private string _name;
public string Name
{
get { return _name; }
set
{
if (string.IsNullOrEmpty(value) || _name == value) return;
_name = value;
Add(Restrictions.Like("Name.FullName", value, MatchMode.Anywhere));
}
}

private DateRange? _registrationDate;
public DateRange? RegsitrationDate
{
get { return _registrationDate; }
set
{
if (_registrationDate == null || _registrationDate.Value.Equals(value.Value)) return;
_registrationDate = value;
AddRange(_registrationDate.Value.BuildCriterion());
}
}
}

public interface ICandidateSearchCriteria : IList<ICriterion>
{
string Name { get; set; }
DateRange? RegsitrationDate { get; set; }
}

To enable easy addition of paging and ordering concerns I've then created some extension methods:

public static DetachedCriteria Build<T> (this IList<ICriterion> list)
{
DetachedCriteria criteria = DetachedCriteria.For<T>();
foreach (ICriterion criterion in list)
{
criteria.Add(criterion);
}
return criteria;
}

public static DetachedCriteria Page (this DetachedCriteria criteria, int pageNumber, int pageSize)
{
criteria.SetMaxResults(pageSize);
criteria.SetFirstResult(pageSize*pageNumber - 1);
return criteria;
}

public static DetachedCriteria OrderBy (this DetachedCriteria criteria, string fieldName, Direction direction)
{
criteria.AddOrder(new Order(fieldName, direction.IsAscending()));
return criteria;
}

The first of these adds a Build extension to an IList<ICriterion>, this produces the DetachedCriteria object. The next two provide for a more 'Fluent' way to set the Paging and Ordering capabilities.

This all get used like this:

ICandidateRepository candidateRepository = MvcApplication.WindsorContainer.Resolve<ICandidateRepository>();
ICandidateSearchCriteria criteria = MvcApplication.WindsorContainer.Resolve<ICandidateSearchCriteria>();
if (!string.IsNullOrEmpty(name)) { criteria.Name = name; }

DetachedCriteria toFire = criteria.Build<Candidate>().OrderBy(sortBy.ToString(), direction).Page(fetchPage, recordsPerPage);

In particular this call is what I like:

criteria.Build<Candidate>().OrderBy(sortBy.ToString(), direction).Page(fetchPage, recordsPerPage);

Having exposed the DetachedCriteria from the searchCriteria class I can simply add the paging and ordering using the new extension methods.

My biggest dislike for this at the moment is that I have needed to reference the DetachedCriteria in my Repository's interface. This bleeding through of NHibernate into my interface is something that I try to minimise. Ideally I'd like to be able to create a repository layer using a different ORM, or even that persists to something other than a relational database, without having to change my interface too much.

Another thing that is unsettling me with this (unfinished) solution is the whole open-closed thing. It was pointed out to me the other day that this approach is not open to extension, and is not closed to modification. I had already tried to accommodate this to an extent by maintaining the SRP (allowing the paging & ordering to be outside of the filtering, and requiring collaboration with the Repository) through exposing the list of ICriterion, thinking that this would allow for some extension.

This is all very much a WIP for me at the moment and I think it'll be a while before I settle on anything. I suspect that I'll be blogging more on this topic before too long.

2 comments:

Unknown said...

I really do like your approach. Did you happen to have any follow-ups?

Sam I Am said...

Like the approach. Having problems with the Direction type:

public static DetachedCriteria OrderBy (this DetachedCriteria criteria, string fieldName, Direction direction)
{
criteria.AddOrder(new Order(fieldName, direction.IsAscending()));
return criteria;
}

Is there something I am missing?