Custom NHibernate Criteria Projections

I recently had the need to investigate a way to do some complicated projections with Criteria queries and I was faced with what seems a common problem: being able to fully select the properties of the root entity of the criteria. An issue has been raised on NHibernate JIRA and there are some questions on StackOverflow about it.

Criteria projections are powerful, but not much documented. I decided to try to understand the problem and find a solution for it. Eventually, I ended um implementing not only a RootEntityProjection but also a generic EntityProjection.

RootEntityProjection selects all the properties of the criteria’s root entity and EntityProjection selects all the properties of any joined entity, selected by its alias.

An example for the RootEntityProjection could be:

   1: var leftJoinAAndB = session.CreateCriteria<EntityA>("a")
   2:     .CreateAlias("PropertyB", "b", JoinType.LeftOuterJoin)
   3:     .Add(Restrictions.IsNotNull("PropertyB"))
   4:     .SetProjection(new RootEntityProjection())
   5:     .SetResultTransformer(Transformers.AliasToBean<EntityA>())
   6:     .List<EntityA>();

And an example for EntityProjection:

   1: var leftJoinAAndB = session.CreateCriteria<EntityA>("a")
   2:     .CreateAlias("PropertyB", "b", JoinType.LeftOuterJoin)
   3:     .Add(Restrictions.IsNotNull("PropertyB"))
   4:     .SetProjection(new EntityProjection<EntityB>("b"))
   5:     .SetResultTransformer(Transformers.AliasToBean<EntityB>())
   6:     .List<EntityB>();

As always, here is the code, first, for RootEntityProjection:

   1: public class RootEntityProjection : IProjection
   2: {
   3:     private readonly List<String> aliases = new List<String>();
   4:     private IType[] columnTypes = null;
   5:  
   6:     protected String[] GetPropertyNames(IClassMetadata classMetadata, ICriteriaQuery criteriaQuery)
   7:     {
   8:         var propertyNames = classMetadata.PropertyNames.Concat(new String[] { classMetadata.IdentifierPropertyName }).Zip(classMetadata.PropertyTypes.Concat(new IType[] { classMetadata.IdentifierType }), (x, y) => new Tuple<String, IType>(x, y)).ToDictionary(x => x.Item1, x => x.Item2).Where(x => !(x.Value is ComponentType) && !(x.Value is CollectionType)).Select(x => x.Key).ToArray();
   9:  
  10:         return (propertyNames);
  11:     }
  12:  
  13:     #region IProjection Members
  14:  
  15:     String[] IProjection.Aliases
  16:     {
  17:         get
  18:         {
  19:             return (this.aliases.ToArray());
  20:         }
  21:     }
  22:  
  23:     String[] IProjection.GetColumnAliases(String alias, Int32 loc)
  24:     {
  25:         throw new NotImplementedException();
  26:     }
  27:  
  28:     String[] IProjection.GetColumnAliases(Int32 loc)
  29:     {
  30:         return (this.aliases.ToArray());
  31:     }
  32:  
  33:     TypedValue[] IProjection.GetTypedValues(ICriteria criteria, ICriteriaQuery criteriaQuery)
  34:     {
  35:         throw new NotImplementedException();
  36:     }
  37:  
  38:     IType[] IProjection.GetTypes(String alias, ICriteria criteria, ICriteriaQuery criteriaQuery)
  39:     {
  40:         throw new NotImplementedException();
  41:     }
  42:  
  43:     IType[] IProjection.GetTypes(ICriteria criteria, ICriteriaQuery criteriaQuery)
  44:     {
  45:         if (this.columnTypes == null)
  46:         {
  47:             var classMetadata = criteriaQuery.Factory.GetClassMetadata(criteria.GetRootEntityTypeIfAvailable());
  48:             var propertyNames = this.GetPropertyNames(classMetadata, criteriaQuery);
  49:  
  50:             this.columnTypes = propertyNames.Select(x => classMetadata.GetPropertyType(x)).ToArray();
  51:         }
  52:  
  53:         return (this.columnTypes);
  54:     }
  55:  
  56:     Boolean IProjection.IsAggregate
  57:     {
  58:         get { return(false); }
  59:     }
  60:  
  61:     Boolean IProjection.IsGrouped
  62:     {
  63:         get { return (false); }
  64:     }
  65:  
  66:     SqlString IProjection.ToGroupSqlString(ICriteria criteria, ICriteriaQuery criteriaQuery, IDictionary<String, IFilter> enabledFilters)
  67:     {
  68:         throw new NotImplementedException();
  69:     }
  70:  
  71:     SqlString IProjection.ToSqlString(ICriteria criteria, Int32 position, ICriteriaQuery criteriaQuery, IDictionary<String, IFilter> enabledFilters)
  72:     {
  73:         var classMetadata = criteriaQuery.Factory.GetClassMetadata(criteria.GetRootEntityTypeIfAvailable());
  74:         var propertyNames = this.GetPropertyNames(classMetadata, criteriaQuery);
  75:         var builder = new SqlStringBuilder();
  76:  
  77:         for (var i = 0; i < propertyNames.Length; ++i)
  78:         {
  79:             var propertyName = propertyNames[i];
  80:             var columnName = criteriaQuery.GetColumn(criteria, propertyName);
  81:  
  82:             builder.Add(columnName);
  83:             builder.Add(" as ");
  84:             builder.Add(propertyName);
  85:  
  86:             this.aliases.Add(propertyName);
  87:  
  88:             if (i < propertyNames.Length - 1)
  89:             {
  90:                 builder.Add(", ");
  91:             }
  92:         }
  93:  
  94:         return (builder.ToSqlString());
  95:     }
  96:  
  97:     #endregion            
  98: }

And EntityProjection (actually, two classes, I created a subclass that is a generic wrapper):

   1: public class EntityProjection : IProjection
   2: {
   3:     private IType[] columnTypes = null;
   4:     private readonly Type rootEntity = null;
   5:     private readonly String alias = null;
   6:  
   7:     protected String[] GetPropertyNames(IClassMetadata classMetadata, ICriteriaQuery criteriaQuery)
   8:     {
   9:         var propertyNames = classMetadata.PropertyNames.Except(criteriaQuery.Factory.GetAllCollectionMetadata().Where(x => x.Key.StartsWith(String.Concat(classMetadata.EntityName, "."))).Select(x => x.Key.Split('.').Last())).Concat(new String[] { classMetadata.IdentifierPropertyName }).ToArray();
  10:  
  11:         return (propertyNames);
  12:     }
  13:  
  14:     public EntityProjection(Type rootEntity, String alias)
  15:     {
  16:         this.rootEntity = rootEntity;
  17:         this.alias = alias;
  18:     }
  19:  
  20:     private readonly List<String> aliases = new List<String>();
  21:  
  22:     #region IProjection Members
  23:  
  24:     String[] IProjection.Aliases
  25:     {
  26:         get
  27:         {
  28:             return (this.aliases.ToArray());
  29:         }
  30:     }
  31:  
  32:     String[] IProjection.GetColumnAliases(String alias, Int32 loc)
  33:     {
  34:         throw new NotImplementedException();
  35:     }
  36:  
  37:     String[] IProjection.GetColumnAliases(Int32 loc)
  38:     {
  39:         return (this.aliases.ToArray());
  40:     }
  41:  
  42:     TypedValue[] IProjection.GetTypedValues(ICriteria criteria, ICriteriaQuery criteriaQuery)
  43:     {
  44:         throw new NotImplementedException();
  45:     }
  46:  
  47:     IType[] IProjection.GetTypes(String alias, ICriteria criteria, ICriteriaQuery criteriaQuery)
  48:     {
  49:         throw new NotImplementedException();
  50:     }
  51:  
  52:     IType[] IProjection.GetTypes(ICriteria criteria, ICriteriaQuery criteriaQuery)
  53:     {
  54:         if (this.columnTypes == null)
  55:         {
  56:             var classMetadata = criteriaQuery.Factory.GetClassMetadata(this.rootEntity);
  57:             var propertyNames = this.GetPropertyNames(classMetadata, criteriaQuery);
  58:  
  59:             this.columnTypes = propertyNames.Select(x => classMetadata.GetPropertyType(x)).ToArray();
  60:         }
  61:  
  62:         return (this.columnTypes);
  63:     }
  64:  
  65:     Boolean IProjection.IsAggregate
  66:     {
  67:         get { return (false); }
  68:     }
  69:  
  70:     Boolean IProjection.IsGrouped
  71:     {
  72:         get { return (false); }
  73:     }
  74:  
  75:     SqlString IProjection.ToGroupSqlString(ICriteria criteria, ICriteriaQuery criteriaQuery, IDictionary<String, IFilter> enabledFilters)
  76:     {
  77:         throw new NotImplementedException();
  78:     }
  79:  
  80:     SqlString IProjection.ToSqlString(ICriteria criteria, Int32 position, ICriteriaQuery criteriaQuery, IDictionary<String, IFilter> enabledFilters)
  81:     {
  82:         var classMetadata = criteriaQuery.Factory.GetClassMetadata(this.rootEntity);
  83:         var propertyNames = this.GetPropertyNames(classMetadata, criteriaQuery);
  84:         var builder = new SqlStringBuilder();
  85:  
  86:         for (var i = 0; i < propertyNames.Length; ++i)
  87:         {
  88:             var propertyName = propertyNames[i];
  89:             var subcriteria = criteria.GetCriteriaByAlias(this.alias);                    
  90:             var columnName = criteriaQuery.GetColumn(subcriteria, propertyName);
  91:  
  92:             builder.Add(columnName);
  93:             builder.Add(" as ");
  94:             builder.Add(propertyName);
  95:  
  96:             this.aliases.Add(propertyName);
  97:  
  98:             if (i < propertyNames.Length - 1)
  99:             {
 100:                 builder.Add(", ");
 101:             }
 102:         }
 103:  
 104:         return (builder.ToSqlString());
 105:     }
 106:  
 107:     #endregion
 108: }
 109:  
 110: public class EntityProjection<T> : EntityProjection
 111: {
 112:     public EntityProjection(String alias) : base(typeof(T), alias)
 113:     {
 114:     }
 115: }

Some explanation is in order:

  1. Through the session factory that is exposed by the ICriteriaQuery interface we get the entity’s class metadata;
  2. From the class metadata we list all of the entity’s mapped properties and id, except those properties that refer to collections;
  3. We cache each property’s NHibernate type for faster access;
  4. A SQL query is built with all the mapped properties.

The two main classes share 90% of their behavior, so it is even possible to have a base class. I leave that for you, dear reader! Smile Hope you find this useful!

                             

No Comments