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