Search code examples
c#nhibernateiqueryablelinq-to-nhibernate

Is it possible to order by class type in NHibnernate IQueryable


It is possible to order by class type in HQL and ICriteria queries using the special keyword class.

criteria.AddOrder("s.class", true);

Can this keyword be used in an IQueryable OrderBy? This seems to only except propery expressions and .GetType() is not supported.


Solution

  • Quick&Dirty suggestion

    A dirty hack may work. In my experience, linq2NH does translate queryables to HQL without much checks (see this question for an example case in its 'trick' sample code). So you may add a dummy and not mapped public virtual string @class { get; set; } property on your base entity then order by it.

    session.Query<YourBaseEntityType>().OrderBy(e => e.@class);
    

    Test it, it is likely it would be translated 'bluntly' to a working HQL query. Of course, it is ugly, may break at any NH version upgrade, ...

    (The @ is required because class is a reserved word in C#. Prefixing with @ tells the compiler it is just a name, not the class keyword.)

    Cleaner suggestion

    A cleaner way would be to extend linq2NH for supporting a Class() method on your entities. (Blog post found on this answer to another question. Edit: here is another interesting implementation example on SO, and here is a list of links which provides much more information on extensibility.)

    Adapting linked above 'extend' blog post to your case, it should be something like following steps.

    First, you need an extension method usable on your entities. Something like:

    public static class YourLinqExtensions
    {
        public static string Class(this YourEntityBaseClass source)
        { 
            // .Net runtime implementation just in case, useless in linq2nhib case
            return source == null ? null : source.GetType().Name;
        }
    }
    

    Then, implements its HQL translation:

    public class ClassGenerator : NHibernate.Linq.Functions.BaseHqlGeneratorForMethod
    {
        public ClassGenerator()
        {
            SupportedMethods = new[]
            {
                NHibernate.Linq.ReflectionHelper.GetMethod(
                    () => YourLinqExtensions.Class(null))
            };
        }
    
        public override NHibernate.Hql.Ast.HqlTreeNode BuildHql(MethodInfo method, 
            Expression targetObject,
            ReadOnlyCollection<Expression> arguments,
            NHibernate.Hql.Ast.HqlTreeBuilder treeBuilder,
            NHibernate.Linq.Visitors.IHqlExpressionVisitor visitor)
        {
            return treeBuilder.Dot(
                // using NHibernate.Hql.Ast; required for .AsExpression() to be found
                // at compile time.
                visitor.Visit(arguments[0]).AsExpression(),
                // Below 'Class' method is not 'YourLinqExtensions.Class' extension
                // method. It is natively available on HqlTreeBuilder.
                treeBuilder.Class());
        }
    }
    

    Extend the default linq2NH registry with your generator:

    public class YourLinqToHqlGeneratorsRegistry:
        NHibernate.Linq.Functions.DefaultLinqToHqlGeneratorsRegistry
    {
        public YourLinqToHqlGeneratorsRegistry()
        {
            // using NHibernate.Linq.Functions; required for .Merge() to be found
            // at compile time.
            this.Merge(new ClassGenerator());
        }
    }
    

    And configure NH to use your new registry. With hibernate.cfg.xml, it should be something like adding following property node under session-factory node:

    <property name="linqtohql.generatorsregistry">YourNameSpace.YourLinqToHqlGeneratorsRegistry, YourAssemblyName</property>
    

    Usage:

    session.Query<YourBaseEntityType>().OrderBy(e => e.Class());
    

    (With the way I have elaborated all of this, it should at least compile, but it is fully untested. Edit: now it is tested.)