Mike Nichols - Son of Nun Technology

October 2006 Entries

Querying Components with NHibernate Criteria

Working with NHibernate's query criteria API is logical, but sometimes I slip. For example, say I have a class Timesheet and the timesheet has property (which is also mapped) named Employee and a component named PayrollPeriod:

public class Timesheet : DomainObject<int>, ITimesheet

{

 

 

    private IEmployee _employee;

    public virtual IEmployee Employee

    {

        get

        {

            return _employee;

        }

    }

 

 

    private IRange<DateTime> _payrollPeriod = new Range<DateTime>(DateTime.MinValue, DateTime.MaxValue);

    public virtual IRange<DateTime> PayrollPeriod

    {

        get

        {

            return _payrollPeriod;

        }

    }

 

 

}


Now, when i query for a timesheet i want to get a timesheet with an Employee.Id = 36.
I might think the Criteria would look like this:


ICriteria crit = session.CreateCriteria(typeof(Timesheet);
crit.Add(Expression.Eq("Employee.Id",36));


Now, that would be fun but it would be wrong. Since Employee is a mapped class, it is really and association. So I neeed to do the following:
ICriteria crit = session.CreateCriteria(typeof(Timesheet);
crit.CreateCriteria("Employee").Add(Expression.Eq("Id",36));


This creates a SubCriteria object via what is called an 'association path' which is really just the property name. This tells NHibby to nest a criteria inside the main Timesheet criteria (inner join).


Now, let's say I have a component inside Timesheet called PayrollPeriod. This PayrollPeriod class might have two properties: Start and End:

    public interface IRange<T>: IValueObject  where T :  IComparable<T>

    {

        T Start { get;}

        T End { get;}

        bool Contains(T valueToFind);

    }

When I want to query on the Timesheet for a PayrollPeriod which matched certain criteria, I will employ the period notation 'Component.PropertyName' to get there. So it would look like this:

ICriteria crit = session.CreateCriteria(typeof(Timesheet);
crit.Add(Expression.Eq("PayrollPeriod.Start",new DateTime(2006,9,9)));

If I were to try to do an association type mapping like we did with Employee, we'd find NHibby complaining because it thinks we are trying to tell it that 'Start' is a property of 'Timesheet'.

A Really Bad Case of OverArchitecture with a high Prematurely Generalized fever

A while back I threw out three options that I had for querying my Db *hopefully* isolating myself from a specific tool (ie, NHibernate) and its API.

DISCLAIMER: This kind of thing isn't worth the money usually.

At the same time I wanted to learn about Dynamic Proxy because of inspiration from a conversation with one genius while reading cool code from another genius.

I had decided against trying to be so generic about getting stuff from my DB simply because the NHibernate API is so RICH and it doesn't make sense to write a parallel universe that will probably never be required. Besides, replacing an OR/M will cause more problems than simply shaking up your QueryObjects.

That said, I sat down and stole ideas from these guys and came up with a fairly rich yet generic way of getting data.

The Query Object Contract

First, I wanted my Domain layer to relate to the following Query Object contract:

 

    public interface IObjectQuery<T>:IOQuery

    {

 

        /// <summary>

        /// The proxied object to interact with using Strong Typing for querying

        /// Usage: query.Where(query.Target.Name).Eq('Mike')

        /// </summary>

        T Target { get;}

        /// <summary>

        /// The where clause in a query, accepting (and ignoring) the strong-typing of <see cref="Target"/> properties

        /// Usage: query.Where(query.Target.Name).Eq('Mike')

        /// </summary>

        /// <param name="ignore"></param>

        /// <returns></returns>

        INamedExpression<T> Where(object ignore);

        /// <summary>

        /// Junction clause between Expressions

        /// Usage: query.Where(query.Target.FirstName).Eq('Mike').And(query.Target.LastName).Eq('Nichols')

        /// </summary>

        /// <param name="ignore"></param>

        /// <returns></returns>

        INamedExpression<T> And(object ignore);

        /// <summary>

        /// Disjuntion clause between Expressions

        /// Usage: query.Where(query.Target.Name).Eq('Mike').Or(query.Target.Name).Eq('John')

        /// </summary>

        /// <param name="ignore"></param>

        /// <returns></returns>

        INamedExpression<T> Or(object ignore);

 

        //Getters/Results

        /// <summary>

        /// Based on the input query expression, count the results

        /// </summary>

        /// <returns></returns>

        int Count(IObjectQuery<T> expression);

        /// <summary>

        /// Get a single result matching the criteria, throw an error if more than one is returned.

        /// </summary>

        /// <returns></returns>

        T Get();

        /// <summary>

        /// Get all objects matching criteria

        /// </summary>

        /// <returns></returns>

        IList<T> GetAll();

    }

 

The Expressions

The expression I need to query with are snagged from Ayende's awesome NHibernate Query Generator he has been developing, adapted for returning my IObjectQuery for fluidity:

    public interface INamedExpression<T>

    {

        IObjectQuery<T> Eq(object value);

 

        IObjectQuery<T> Between(object lo, object hi);

 

 

        IObjectQuery<T> EqProperty(object ignore);

 

        IObjectQuery<T> Ge(object value);

 

        IObjectQuery<T> Gt(object value);

 

        IObjectQuery<T> In(ICollection values);

 

 

        IObjectQuery<T> In(params object[] values);

 

        IObjectQuery<T> InsensitiveLike(object value);

 

 

        IObjectQuery<T> IsNotNull();

 

        IObjectQuery<T> IsNull();

 

 

        IObjectQuery<T> Le(object value);

 

 

        IObjectQuery<T> LeProperty(object ignore);

 

 

        IObjectQuery<T> Like(object value);

 

 

        IObjectQuery<T> Lt(object value);

 

 

        IObjectQuery<T> LtProperty(object ignore);

 

        INamedExpression<T> Not();

 

    }

Using these two contracts, I can write the following example queries

query.Count(query.Where(query.Target.Parent.Id).Eq(item)) > 0

query.Where(query.Target.Title).EqProperty(query.Target.Name);

query.Where(query.Target.Name).Not().EqProperty(query.Target.Title);

query.Where(query.Target.Name).Not().Eq("boing")

                .And(query.Target.Title).Eq("stud")

                .Or(query.Target.Title).Eq("jumper");

The Query Object Implementation

Now these are the contracts my Domain will consume. I'm using Castle to inject the implementation of IObjectQuery<T> at runtime so mostly I have shielded my domain from the NHibernate API and created strongly-typed queries. This all came about discussing Ayende's syntax on Rhino Mocks as a neat way of avoiding strings for property names. By intercepting thes calls to 'Target' with Dynamic Proxy, we can build our Expressions and (hopefully) create a fairly rich interface that responds to refactoring tools and remains ORM agnostic. This certainly won't meet all needs but at least most general querying that I have found. It even accepts chained nested criteria requirements, like query.Target.CustomProperty1.CustomProperty2.Name.Eq('dude'). This is because my persisted entities all inherit from a layer superclass so during the proxy interception I can determine whether I need to Create a SubCriteria. All this logic lives in the same assembly with my NHibernate implementations.

First, the main ObjectQuery<T> implementation with its nested NamedExpression implementation. Not completely refactored yet, but workable. I use State to keep track of junctions, disjunctions, and the like.

    [Transient]

    public class ObjectQuery<T> : IObjectQuery<T>

    {

        #region Fields

        private readonly T _target;

 

        private readonly QueryInterceptor<T> _interceptor;

        private ICriteria _currentCriteria;

        /// <summary>

        /// Used by <see cref="RemoveLastCriterion"/> for joining or disjoining expressions during State .Adds.

        /// </summary>

        private ICriterion _lastCriterion;

        /// <summary>

        /// Collection of <see cref="IProjection"/> types attached to this query.

        /// </summary>

        private readonly ProjectionList _projectionList = Projections.ProjectionList();

 

        private List<ICriterion> _criterionCache = new List<ICriterion>();

 

        #endregion

 

        public ObjectQuery()

        {

            Initialize();

            //Setup our proxy object for strong typing

            _interceptor = new QueryInterceptor<T>(this);

            ProxyGenerator g = new ProxyGenerator();

            _target = (T)g.CreateClassProxy(typeof(T), _interceptor);

        }

        private void Initialize()

        {

            //Get our criteria Impl

            IUnitOfWorkManager mgr = IoC.Resolve<IUnitOfWorkManager>();

            _currentCriteria = ((UnitOfWork)mgr).CurrentNHibernateSession.CreateCriteria(typeof(T));

 

            //Initialize the state to WhereState<T>

            _state = new WhereState<T>(this);

 

        }

 

        /// <summary>

        /// The most recent <see cref="ICriteria"/> object in scope. When chaining

        /// ICriteria objects, the last one can be used to .List() or .UniqueResult()

        /// </summary>

        public ICriteria CurrentCriteria

        {

            get

            {

                return _currentCriteria;

            }

        }

        /// <summary>

        /// Attaches any criterion that have been cached and clear the cache,

        /// set <see cref="CurrentCriteria"/> to the subcriteria object,

        /// then set the _state reference to <see cref="SubQueryState<T>"/>.

        /// </summary>

        /// <returns></returns>

        public ICriteria CreateSubCriteria()

        {

            //First attach any criterion to our current criteria

            AttachCriterionFromCacheToCurrentCriteria();

            //Now set our current criteria to the new nested subcriteria

            _currentCriteria = _currentCriteria.CreateCriteria(LastCall.First);

            //Set our state to SubQueryState in case 'And()' or 'Or()' is called...this would erroneously join the last criterion expression with the new one

            _state = new SubQueryState<T>(this);

 

            return _currentCriteria;

        }

 

        /// <summary>

        /// List of <see cref="ICriterion"/> that will be attached to the <see cref="CurrentCriteria"/> object at

        /// the last moment. This cache allows us to chain , junction, and disjunction <see cref="ICriterion"/> to a

        /// <see cref="ICriteria"/> instance.

        /// </summary>

        public List<ICriterion> CriterionCache

        {

            get { return _criterionCache; }

        }

        /// <summary>

        /// A kind of 'flush' for the <see cref="CurrentCriteria"/>. First it attaches any <see cref="ICriterion"/>

        /// that have been added to the <see cref="CriterionCache"/> and then it clears that cache.

        /// </summary>

        public void AttachCriterionFromCacheToCurrentCriteria()

        {

            foreach (ICriterion criterion in _criterionCache)

            {

                _currentCriteria.Add(criterion);

            }

            _criterionCache.Clear();

 

        }

        private QueryState<T> _state;

        private Pair<string,Type> LastCall

        {

            get

            {

                if (!new NonEmptyStringSpecification().IsSatisfiedBy(_interceptor.LastCall.First))

                    throw new InvalidOperationException("A property has not been referenced for applying criteria.");

                return _interceptor.LastCall;  

            }

        }

        private Pair<string,Type> PreviousCall

        {

            get

            {

                if (!new NonEmptyStringSpecification().IsSatisfiedBy(_interceptor.PreviousCall.First))

                    throw new InvalidOperationException("A property has not been referenced for applying criteria to compare against.");

 

                return _interceptor.PreviousCall;

            }

        }

        /// <summary>

        /// Adds <see cref="ICriterion"/> using the current state of the query. At the same time, it assigns the

        ///