Mike Nichols - Son of Nun Technology

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

        /// <see cref="ICriterion"/> coming from the _state to the <see cref="_lastCriterion"/> field.

        /// </summary>

        /// <param name="criterion"></param>

        public void AddCriterion(ICriterion criterion)

        {

            _lastCriterion = _state.AddCriterion(criterion);

 

 

        }

        public void SetSubQuery()

        {

            _state = new SubQueryState<T>(this);

        }

        /// <summary>

        /// Called from <see cref="_state"/> reference for clearing the <see cref="_lastCriterion"/>

        /// field and getting its reference for junction and disjunction criterion.

        /// </summary>

        /// <returns></returns>

        public ICriterion RemoveLastCriterion()

        {

            ICriterion last = _lastCriterion;

            _criterionCache.Remove(last);

            return last;

        }

        /// <summary>

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

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

        /// </summary>

        public T Target

        {

            get { return _target; }

        }

        /// <summary>

        ///<