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 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
///