Tuesday, March 25, 2008

Strongly typed NHibernate Criteria with C# 3

I, like many others, hate string literals. Of course, I mean string literals that have some meaning to the code itself, not the "hello, world" kind of strings. They are not type-safe, not easy to refactor, etc. The drawbacks have been mentioned a lot.

In particular, it always bothered me that NHibernate suffers from this problem, specially in the Criteria API. Well, now with C# 3 AST manipulation we can easily fix this. I wrote a simple set of helper classes and extension methods that provide an Expression parameter wherever there was a string parameter describing a property name. Let's see an example:

Instead of writing this:

IList cats = sess.CreateCriteria(typeof(Cat))
    .Add( Expression.Like("Name", "F%")
    .AddOrder( Order.Asc("Name") )
    .AddOrder( Order.Desc("Age") )
    .SetMaxResults(50)
    .List();

You can write this:

IList cats = sess.CreateCriteria(typeof(Cat))
    .Add( ExpressionEx.Like((Cat c) => c.Name, "F%")
    .AddOrder( OrderEx.Asc((Cat c) => c.Name) )
    .AddOrder( OrderEx.Desc((Cat c) => c.Age) )
    .SetMaxResults(50)
    .List();

which is a bit longer, but refactorable.

Code is here (NHibernate 1.2)

UPDATE 2/21/2009: Dan Miser has kindly submitted a patch to port these extensions to NHibernate 2.0.1GA. Code is here. Thanks Dan!

UPDATE 3/28/2010: Two years have passed now since I originally published this and several similar solutions have popped up. In particular, I recommend NH Lambda extensions for NHibernate 2.x which seems to be more complete, less verbose, and better maintained than my own solution. NHibernate 3 will have a new official API similar to this, named QueryOver. If you're stuck with NHibernate 1.2 the only solution available is the one presented in this article.

Related projects:

7 comments:

Dan said...

Have you considered using the NHQG stuff Ayende wrote (I did see you linked to his NHQG article in fact)? You have to buy into the Repository of T stuff (as far as I know) but then you can write stuff like:

Repository< Cat >.FindAll(Where.Cat.Name.InsensitiveLike("F", MatchMode.Anywhere, OrderBy.Cat.Name.Asc && OrderBy.Cat.Age.Asc)

mausch said...

Dan, I have considered the two projects that I link to. NHQG is a great code generator, but I try to avoid generators whenever I can. LINQ to NHibernate is still being developed and not ready for production AFAIK... So... this is just a lightweight alternative to those two.

darioquintana said...

Nice feature. But remember that you have to use criteria in dynamics-complex queries. I think that right now isn't a option use criteria instead HQL. When you write the HQL in the mapping NHibernate make a verification (if the HQL is wrong an exception will be thorwn) on BuildSessionFactory time and then the query is cached, this is, parsed once and used forever, and with criteria NH need parse the query every time. On Linq To NHibernate this thing must change...but in a future.

Best regards

Dan Miser said...

I upgraded the Extensions to the 2.0GA release of NHibernate fairly easily (added namespaces, adjusted Expression.Eq to use Criterion.Expression.Eq, and updated the birhtdate test for my culture).

If you want me to send a patch, or something else, let me know. dmiser@wi.rr.com.

Dan

mausch said...

@Dan: by all means, patches are always welcome! I'm sending you a mail

davidmus said...

Hi Mauricio. Great stuff, thanks! However, we hit a snag using this. We had the following IQuery:

from FooBar f where f.Foo.Id = :fooId and f.Bar.Id = :barId

With this, we tried the following when converting to a criteria search instead:

.Add( ExpressionEx.Eq((FooBar f) => f.Foo.Id, fooId)

This mapped to a member name of "Id" though. We've added a fix to your ObjectExtensions class that works here, and would like to ping it your way in case you want to incorporate it. Also, one of your unit tests is locale specific, and will fail in many countries outside the US - we've got a fix
for that to ping you as well. Drop me an email at david (at) musgroves.us and I'll send them to you.

David

mausch said...

@davidmus: thanks! I'm sending you a mail