Search code examples
nhibernatelinq-to-nhibernate

How do I get NHibernate to generate SQL for a computed property?


I've Googled around and come up with sample code, but it's giving me trouble. Here's what I've got, based on what I found:

In the persistent class I have

public static readonly Expression<Func<Detail, decimal>> TotExpression = d =>
    (decimal)((d.Fee == null ? 0 : d.Fee) + (d.Expenses == null ? 0 : d.Expenses));

public static Func<Detail, decimal> CompiledTot => TotExpression.Compile();
public virtual decimal Tot => CompiledTot(this);

I register the property using

class ComputedPropertyGeneratorRegistry : DefaultLinqToHqlGeneratorsRegistry
{
    public ComputedPropertyGeneratorRegistry()
    {
        CalculatedPropertyGenerator<Detail, decimal>.Register(
            this,
            x => x.Tot,
            Detail.TotExpression);
    }
}

public class CalculatedPropertyGenerator<T, TResult> : BaseHqlGeneratorForProperty
{
    public static void Register(ILinqToHqlGeneratorsRegistry registry, Expression<Func<T, TResult>> property, Expression<Func<T, TResult>> calculationExp)
    {
        registry.RegisterGenerator(ReflectHelper.GetProperty(property), new CalculatedPropertyGenerator<T, TResult> { _calculationExp = calculationExp });
    }
    private CalculatedPropertyGenerator() { } // Private constructor

    private Expression<Func<T, TResult>> _calculationExp;
    public override HqlTreeNode BuildHql(MemberInfo member, Expression expression, HqlTreeBuilder treeBuilder, IHqlExpressionVisitor visitor)
    {
        return visitor.Visit(_calculationExp);
    }
}

And in my session-factory configuration I have

cfg.LinqToHqlGeneratorsRegistry<ComputedPropertyGeneratorRegistry>();

Yet when I run

session.Query<Detail>().Select(x => x.Tot).First();

I get

NHibernate.Hql.Ast.ANTLR.InvalidPathException: Invalid path: 'd.Fee'

It seems that when NH tries to generate the SQL it calls, at some point, LiteralProcessor.LookupConstant on d.Fee, which calls ReflectHelper.GetConstantValue("d.Fee"), which for some reason assumes that "d" is the name of the class to which the property belongs. Of course it isn't, which breaks everything. I have no idea why it's going down this wrong path.


Solution

  • Okay, the problem seems to be that the HQL generator returns an expression where the 'd' parameter isn't treated as a parameter, so the resultant HQL doesn't know what to do with it. If I change the 'x' parameter in my query to 'd', as in

    session.Query<Detail>().Select(d => d.Tot).First();
    

    it all hangs together. This is obviously a bother, but not enough of one to outweigh being able to search on and select computed properties. I assume someone who understands the HQL "visitor" better would be able to make the proper adjustments in the HQL generator, but I'll leave that for some other volunteer.

    UPDATE: I couldn't leave it at that, so I cobbled together a way to do it, with the help of some code from Phil Klein.

    Phil's provided this class

    public class PredicateRewriter
    {
        public static Expression<Func<T, TResult>> Rewrite<T, TResult>(Expression<Func<T, TResult>> exp, string newParamName)
        {
            var param = Expression.Parameter(exp.Parameters[0].Type, newParamName);
            var newExpression = new PredicateRewriterVisitor(param).Visit(exp);
    
            return (Expression<Func<T, TResult>>)newExpression;
        }
    
        private class PredicateRewriterVisitor : ExpressionVisitor
        {
            private readonly ParameterExpression _parameterExpression;
    
            public PredicateRewriterVisitor(ParameterExpression parameterExpression)
            {
                _parameterExpression = parameterExpression;
            }
    
            protected override Expression VisitParameter(ParameterExpression node)
            {
                return _parameterExpression;
            }
        }
    }
    

    which I use here

        public override HqlTreeNode BuildHql(MemberInfo member, Expression expression, HqlTreeBuilder treeBuilder, IHqlExpressionVisitor visitor)
        {
            // this is a kludge because I don't know how to pry the name out of the parameter expression
            var inside = new Regex("\\[(.*)\\]");
            var name = inside.Match(expression.ToString()).Groups[1].Value;
            return visitor.Visit(PredicateRewriter.Rewrite<T, TResult>(_calculationExp, name));
        }
    

    It's not going to work if the expression has multiple parameters, but that rarely happens and I'm really not looking to make a career of refining this :).

    UPDATE: I have a resolution. Rather than subclassing DefaultLinqToHqlGeneratorsRegistry, which seems to be too far down the chain of converting LINQ to SQL, I subclassed DefaultQueryProvider, IQueryProviderWithOptions and inserted it with cfg.LinqQueryProvider<CustomQueryProvider>().

    This is based on code from Ivan Stoev that I found here.