Implementing Linq for NHibernate - Part 2 (Ordering and Paging)

Published 29 March 07 10:14 PM | bdiaz 

Following Ayende's lead, I am going to continue to explain how we are implementing the Linq provider for NHibernate.  You can read the first part of this series on his blog:
http://ayende.com/Blog/archive/2007/03/17/Implementing-Linq-for-NHibernate-A-How-To-Guide--Part.aspx

Update 04/03/2007: Implementing Linq for NHibernate - Part 3 (Aggregate and Element Operators)

A custom Linq Provider can be created by implementing the System.Linq.IQueryable<T> interface that exposes some type of data store.  The NHibernate.Linq.NHibernateLinqQuery<T> class provides access to an underlying database via an NHibernate Session object using Criteria Queries.

I have added several new query method handlers to support slightly more complex queries.  They are called from within the CreateQuery<TElement> method (noted in bold) when a LINQ query is executed.  The method names match the names of the Standard Query Operators found in the System.Linq.Queryable type.  A LINQ query will be transformed into these method calls at compile time.

public IQueryable<TElement> CreateQuery<TElement>(LinqExpression expression)
{
    MethodCallExpression call = (MethodCallExpression) expression;
    switch ( call.Method.Name )
    {
        case "Where":
            HandleWhereCall(call);
            AddCurrentCriterions();
            break;
        case "Select":
            HandleSelectCall(call);
            AddCurrentCriterions();
            break;
        case "OrderBy":
        case "ThenBy"
:
            HandleOrderByCall(call);
            break
;
        case "OrderByDescending"
:
        case "ThenByDescending"
:
            HandleOrderByDescendingCall(call);
            break
;
        case "Take"
:
            HandleTakeCall(call);
            break
;
        case "Skip"
:
            HandleSkipCall(call);
            break
;
        case "Distinct"
:
            HandleDistinctCall(call);
            break
;
        default:
            throw new Exception("The method " + call.Method.Name + " is not implemented.");
    }

     return new NHibernateLinqQuery<TElement>(session, rootCriteria, criterionStack);
}

The OrderBy and ThenBy operators are both passed to the HandleOrderByCall method, which adds an Order.Asc expression for the property name specified in the method call's operand expression.  In a similar fashion, the OrderByDesceding and ThenByDescending operators are passed to the HandleOrderByDescendingCall method that adds an Order.Desc expression to the rootCriteria.

private void HandleOrderByCall(MethodCallExpression call)
{
    var expr = ((UnaryExpression) call.Arguments[1]).Operand;
    rootCriteria.AddOrder(NHibernate.Expression.
Order.Asc(GetMemberName(expr)));
}

private void HandleOrderByDescendingCall(MethodCallExpression call)
{
    var expr = ((UnaryExpression) call.Arguments[1]).Operand;
    rootCriteria.AddOrder(NHibernate.Expression.
Order.Desc(GetMemberName(expr)));
}

This allows us to write these kinds of queries:

var query = from c in nwnd.Customers
                 orderby c.CustomerID descending
                 select c.CustomerID;

The Take and Skip operators allow developers to create higher performance queries when dealing with paged data scenarios.  Their corresponding handler methods apply the first result and max results restrictions for the criteira query.

private void HandleTakeCall(MethodCallExpression call)
{
    int count = (int) ((ConstantExpression) call.Arguments[1]).Value;
    rootCriteria.SetMaxResults(count);
}

private void HandleSkipCall(MethodCallExpression call)
{
    int index = (int) ((ConstantExpression) call.Arguments[1]).Value;
    rootCriteria.SetFirstResult(index);
}

Here is how you would use these methods in a query:

var query = ( from c in nwnd.Customers select c.CustomerID ).Skip(10).Take(10).ToList();

Finally, distinct queries can be very useful to determine a unique set of values in a given set of data.  The Distinct operator is passed to the following method so that we can add a Distinct Projection to the rootCriteria.

private void HandleDistinctCall(MethodCallExpression call)
{
    var projection = GetProjection<NHibernate.Expression.PropertyProjection>();
    if ( projection != null )
    {
        rootCriteria.SetProjection(
            NHibernate.Expression.
Projections.Distinct(
                NHibernate.Expression.
Projections.Property(projection.PropertyName)));
    }
}

This is a distinct query in action:

var q = ( from c in db.Customers select c.City ).Distinct();

Whew!  That was a brain full.  If you have any questions regarding the NHibernate classes being used, please refer to the online documentation.  The really great part of this exercise is discovering that the NHibernate Criteria API lends itself well to this type of implementation.  While there may be some scenarios that are going to be difficult to support, the Criteria Queries will be able to handle a majority of our requirements.  So what are you waiting for?  Go and grab the latest bits!

svn co https://rhino-tools.svn.sourceforge.net/svnroot/rhino-tools/experiments/NHibernate.Linq/

Stay tuned for the next part in the series where I will be covering Aggregate and Element operators.

Filed under: , ,
New Comments to this post are disabled