Showing posts with label NHibernate. Show all posts
Showing posts with label NHibernate. Show all posts

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.

Sunday, 28 September 2008

Testing DateTime Type Properties for Value Equality in Integration Tests

I've just been writing some integration tests to ensure that some Repositories I'm writing work with NHibernate properly, basically ensuring that the mappings do what I want. The Repository classes in question use the latest release of NHibernate (2.0.0.GA) for their implementation and I'm using MbUnit for my integration tests.

To try and save on the number of asserts that I have to write I thought that I'd try to use the Assert.AreValueEqual method that MbUnit provides. This allows me to write this in my test:

foreach (PropertyInfo propertyInfo in typeof(Candidate).GetProperties())
{
Assert.AreValueEqual(propertyInfo,expected,actual, null);
}

The context for this test is that I'm persisting a copy of a new Candidate object to the database (using NHibernate, but not my repository) which exposes a property, RegistrationDate, of the type ?DateTime. When this property is cycled through to the Assert always fails. Initially this puzzled me as the two DateTime instances appeared to be the same. A quick look at the DateTime.Equals(DateTime) method in reflector soon revealed that when this operation is called it is the InternalTicks property that is the subject of the comparison:

return (this.InternalTicks == value.InternalTicks);

When I get the DateTime back from the database this is not as precise as it was originally in the world of the CLR. The expected (original) value is 633581595292333682 whilst the actual (retrieved) value is 633581595290000000. This isn't enough of a difference to be significant for my purposes, as the ToString() representations are Equal and I don't need a level of accuracy beyond days, let alone fractions of a second.

I figure my options here are:

  1. Don't use Assert.AreValueEqual and hand write each Assertion as an Assert.AreEqual instead;
  2. Store the DateTime in the database as the value of the InternalTicks rather than the DateTime value itself. The obvious problem I can see with this is that any extraction, such as to a reporting database, would become more problematic if this was done. I figure there are times when testing should inform design, but this is clearly not one of them!
  3. Isolate NullableDateTime type properties and deal with these differently.

By changing the code a little I am able to isolate DateTime properties and deal with these using the ToString() method like this:

Type NullableDateTimeType = Type.GetType("System.Nullable`1[[System.DateTime, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]");
foreach (PropertyInfo propertyInfo in typeof(Candidate).GetProperties())
{
if (propertyInfo.PropertyType == NullableDateTimeType)
{
DateTime expectedDateTime = (DateTime)propertyInfo.GetValue(expected, null);
DateTime actualDateTime = (DateTime)propertyInfo.GetValue(expected, null);
Assert.AreEqual(expectedDateTime.ToString(), actualDateTime.ToString(), "The expected RegistrationDate is not equal to the actual RegistrationDate.");
}
else
{
Assert.AreValueEqual(propertyInfo, expected, actual, null);
}
}

I'm not too sure how much coding I've saved myself with this, but I figure that as the number of properties involved increases it will reduce maintenance. If anyone out there has a better solution (doing this at gone 2 in the morning makes me think that there probably is a better solution) then please feel free to suggest it.

Saturday, 23 August 2008

NHibernate 2.0 is Finally Released

I've just read on Ayende's blog that the final release of NHibernate 2.0 is now available. I've just got the latest release and TortoiseSVN just finished getting me the latest code now.

That's my bank holiday sorted!