Implementing Linq for NHibernate - Part 3 (Aggregate and Element Operators)

Published 03 April 07 04:27 PM | bdiaz 

The previous posts in this series can be found here:
Part 1 - Implementing Linq for NHibernate (by Ayende)
Part 2 - Ordering and Paging

In this installment I will be covering the Execute<TElement> method of the IQueryable<T> interface and how it is used to return aggregate values from a LINQ query.  This method is also used to execute queries that use Element Operators such as First(), Single(), etc.  First, I will show the Execute<TElement> method as it has been implemented thus far:

public TElement Execute<TElement>(System.Linq.Expressions.Expression expression)
{
    MethodCallExpression call = (MethodCallExpression) expression;
    switch ( call.Method.Name )
    {
        case "First":
            return First<TElement>();
        case "FirstOrDefault":
            return FirstOrDefault<TElement>();
        case "Single":
            return Single<TElement>();
        case "SingleOrDefault":
            return SingleOrDefault<TElement>();
        case "Average":
        case "Count":
        case "LongCount":
        case "Max":
        case "Min":
        case "Sum":
            return Aggregate<TElement>(call);
        default:
            throw new Exception("The method " + call.Method.Name + " is not implemented.");
    }
}

We have chosen to implement the Execute<TElement> method just as we did the CreateQuery<T> method, by passing the MethodCallExpression to a handler method based on the method's name.  Please note that the TElement type for this method will not necessarily match the type T of the IQueryable<T> instance because the Execute<TElement> method may return a projection of the data instead of an entire object graph.

private TElement First<TElement>()
{
    return rootCriteria.SetFirstResult(0)
        .SetMaxResults(1)
        .List<TElement>()
        .First();
}

private TElement FirstOrDefault<TElement>()
{
    return rootCriteria.SetFirstResult(0)
        .SetMaxResults(1)
        .List<TElement>()
        .FirstOrDefault();
}

The First<TElement> and FirstOrDefault<TElement> methods set a max results restriction on the rootCriteria so that a "SELECT TOP 1..." or similar query is executed limiting the result set to no more than one record.  The difference in the two methods is that the First<TElement> method will throw an exception if no records were returned while the FirstOrDefault<TElement> method will return a default value for value types or null, otherwise.

private TElement Single<TElement>()
{
    return rootCriteria.SetFirstResult(0)
        .SetMaxResults(2)
        .List<TElement>()
        .Single();
}

private TElement SingleOrDefault<TElement>()
{
    return rootCriteria.SetFirstResult(0)
        .SetMaxResults(2)
        .List<TElement>()
        .SingleOrDefault();
}

The Single<TElement> and SingleOrDefault<TElement> methods operate just like the First<TElement> and FirstOrDefault<TElement> methods respectively, except for the fact that an exception will be thrown if more than one record is returned by the query.  And here are some example queries for the Element Operators:

Shipper shipper = db.Shippers.First();

Customer cust = db.Customers.SingleOrDefault(c => c.CustomerID == "BONAP");

Currently, the Aggregate<TElement> method handles the situations where a developer requires an Average, Count, LongCount, Max, Min, or Sum calculation to be performed across the data set returned by the current query.  It does this by making a call to GetProjection, which is a utility method that translates the LINQ MethodCallExpression into the appropriate NHibernate Projection, and then setting the projection of the rootCriteria object.

private TElement Aggregate<TElement>(MethodCallExpression call)
{
    var projection = GetProjection(call);
    TElement value =
default(TElement);
    if ( projection != null )
    {
        object result = rootCriteria.SetProjection(projection).UniqueResult();
        value = (TElement)
LinqUtil.ChangeType(result, typeof(TElement));
    }
    return value;
}

private NHibernate.Expression.IProjection GetProjection(MethodCallExpression call)
{
    NHibernate.Expression.
IProjection projection = null;
    string propertyName = null;

    if ( call.Arguments.Count > 1 )
    {
        propertyName = GetMemberName(call.Arguments[1]);
    }
    else
    {
        var proj = GetProjection<NHibernate.Expression.PropertyProjection>();
        if ( proj != null )
        {
            propertyName = proj.PropertyName;
        }
    }

    if ( String.IsNullOrEmpty(propertyName) )
    {
        switch ( call.Method.Name ) 
        {
            case "Count":
            case "LongCount":
                projection = NHibernate.Expression.
Projections.RowCount();
                break;
            default:
                throw new Exception("The aggregate method " + call.Method.Name + " is not implemented.");
        }
    }
    else
    {
        switch ( call.Method.Name )
        {
            case "Average":
                projection = NHibernate.Expression.
Projections.Avg(propertyName);
                break;
            case "Count":
            case "LongCount":
                if ( call.Arguments.Count > 1 )
                {
                    VisitExpression(call.Arguments[1]);
                    projection = NHibernate.Expression.
Projections.RowCount();
                }
                else
                {
                    projection = NHibernate.Expression.
Projections.Count(propertyName);
                }
                break;
            case "Max":
                projection = NHibernate.Expression.
Projections.Max(propertyName);
                break;
            case "Min":
                projection = NHibernate.Expression.
Projections.Min(propertyName);
                break;
            case "Sum":
                projection = NHibernate.Expression.
Projections.Sum(propertyName);
                break;
            default:
                throw new Exception("The aggregate method " + call.Method.Name + " is not implemented.");
        }
    }
    return projection;
}

Here are a few examples of LINQ queries using the various Aggregate Operators:

var q = db.Customers.Count();

var
q = db.Products.Sum(p => p.UnitsOnOrder);

var q = db.Orders.Select(o => o.Freight).Average();

Hopefully this series is helping to shed some light on the inner workings of the LINQ Provider.  As always, any comments or suggestions are welcome.

Thank you.

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