Search code examples
c#nhibernateexpression-trees

Custom LinqToHqlGeneratorsRegistry - InvalidCastException: 'Unable cast "Antlr.Runtime.Tree.CommonTree" to "NHibernate.Hql.Ast.ANTLR.Tree.IASTNode"


I make my own implementation of LinqToHqlGeneratorsRegistry for using specification pattern in my models. I can use specification with objects and query and do not repeat code (see sample). You can see all code here. My code work fine with all cases except one. I got InvalidCastException if specification contains DateTime variable.

    public class Client
    {
        public static readonly Specification<Client> IsMaleSpecification = new Specification<Client>(x => x.Sex == "Male");

        public static readonly Specification<Client> IsAdultSpecification = new Specification<Client>(x => x.Birthday < DateTime.Today);

        [Specification(nameof(IsAdultSpecification))]
        public virtual bool IsAdult => IsAdultSpecification.IsSatisfiedBy(this);

        [Specification(nameof(IsMaleSpecification))]
        public virtual bool IsMale => IsMaleSpecification.IsSatisfiedBy(this);
    }

...
  var client = new Client() {Sex = "Male"};
  var isMale = client.IsMale; //true

  var maleCount = session.Query<Client>().Count(x => x.IsMale); //ok

  var adultCount = session.Query<Client>().Count(x => x.IsAdult);//exception
...

Exception

   в NHibernate.Hql.Ast.ANTLR.HqlSqlWalker.addrExprDot(Boolean root)
   в NHibernate.Hql.Ast.ANTLR.HqlSqlWalker.addrExpr(Boolean root)
   в NHibernate.Hql.Ast.ANTLR.HqlSqlWalker.expr()
   в NHibernate.Hql.Ast.ANTLR.HqlSqlWalker.exprOrSubquery()
   в NHibernate.Hql.Ast.ANTLR.HqlSqlWalker.comparisonExpr()
   в NHibernate.Hql.Ast.ANTLR.HqlSqlWalker.logicalExpr()
   в NHibernate.Hql.Ast.ANTLR.HqlSqlWalker.whereClause()
   в NHibernate.Hql.Ast.ANTLR.HqlSqlWalker.unionedQuery()
   в NHibernate.Hql.Ast.ANTLR.HqlSqlWalker.query()
   в NHibernate.Hql.Ast.ANTLR.HqlSqlWalker.selectStatement()
   в NHibernate.Hql.Ast.ANTLR.HqlSqlWalker.statement()
   в NHibernate.Hql.Ast.ANTLR.HqlSqlTranslator.Translate()
   в NHibernate.Hql.Ast.ANTLR.QueryTranslatorImpl.Analyze(String collectionRole)
   в NHibernate.Hql.Ast.ANTLR.QueryTranslatorImpl.DoCompile(IDictionary`2 replacements, Boolean shallow, String collectionRole)
   в NHibernate.Hql.Ast.ANTLR.ASTQueryTranslatorFactory.CreateQueryTranslators(IASTNode ast, String queryIdentifier, String collectionRole, Boolean shallow, IDictionary`2 filters, ISessionFactoryImplementor factory)
   в NHibernate.Hql.Ast.ANTLR.ASTQueryTranslatorFactory.CreateQueryTranslators(IQueryExpression queryExpression, String collectionRole, Boolean shallow, IDictionary`2 filters, ISessionFactoryImplementor factory)
   в NHibernate.Engine.Query.QueryPlanCache.GetHQLQueryPlan(IQueryExpression queryExpression, Boolean shallow, IDictionary`2 enabledFilters)
   в NHibernate.Impl.AbstractSessionImpl.GetHQLQueryPlan(IQueryExpression queryExpression, Boolean shallow)
   в NHibernate.Impl.AbstractSessionImpl.CreateQuery(IQueryExpression queryExpression)
   в NHibernate.Linq.DefaultQueryProvider.PrepareQuery(Expression expression, IQuery& query)
   в NHibernate.Linq.DefaultQueryProvider.Execute(Expression expression)
   в NHibernate.Linq.DefaultQueryProvider.Execute[TResult](Expression expression)
   в System.Linq.Queryable.Count[TSource](IQueryable`1 source, Expression`1 predicate)
   в ConsoleApp1.Program.Main(String[] args) в C:\git\TestApp\ConsoleApp1\Program.cs:строка 32

Why specification with any another type variables work fine?


Solution

  • The particular problem is not the DateTime type, but the DateTime.Today method.

    The general problem is that the HqlGenerators are called too late in the NHibernate LINQ query expression processing pipeline, so many parts of the original expression preprocessing like partial evaluation, parameterization etc. are missing there. The difference can easily be seen even with the "working" query - if you use directly x => x.Sex == "Male" inside the LINQ query, the SQL query is parameterized, while the translated SQL from x => x.IsMale uses constant literal.

    What you are trying to achieve is basically replacing one expression with another inside expression tree, which is exactly what ExpressionVisitors are for. And all you need is to be able to preprocess the query expression before the query provider.

    Strangely, none of the major LINQ query providers (NHibernate, EF6, EF Core) provide a way to do that. But more on this later. Let me first show the method needed to apply the specifications (error checking omitted):

    public static class SpecificationExtensions
    {
        public static Expression ApplySpecifications(this Expression source) =>
            new SpecificationsProcessor().Visit(source);
    
        class SpecificationsProcessor : ExpressionVisitor
        {
            protected override Expression VisitMember(MemberExpression node)
            {
                if (node.Expression != null && node.Member is PropertyInfo property)
                {
                    var info = property.GetCustomAttribute<SpecificationAttribute>();
                    if (info != null)
                    {
                        var type = property.DeclaringType;
                        var specificationMemberInfo = type.GetFields(BindingFlags.Static | BindingFlags.Public)
                            .Single(x => x.Name == info.FieldName);
                        var specification = (BaseSpecification)specificationMemberInfo.GetValue(null);
                        var specificationExpression = (LambdaExpression)specification.ToExpression();
                        var expression = specificationExpression.Body.ReplaceParameter(
                            specificationExpression.Parameters.Single(), Visit(node.Expression));
                        return Visit(expression);
                    }
                }
                return base.VisitMember(node);
            }
        }
    }
    

    which uses the following helper:

    public static partial class ExpressionExtensions
    {
        public static Expression ReplaceParameter(this Expression source, ParameterExpression from, Expression to)
            => new ParameterReplacer { From = from, To = to }.Visit(source);
    
        class ParameterReplacer : ExpressionVisitor
        {
            public ParameterExpression From;
            public Expression To;
            protected override Expression VisitParameter(ParameterExpression node) => node == From ? To : base.VisitParameter(node);
        }
    }
    

    Now the plumbing part. Actually NHibernate allows you to replace the LINQ provider with your own. In theory you should be able to create DefaultQueryProvider derived class, override the PrepareQuery method and preprocess the passed expression before calling the base implementation.

    Unfortunately there is an implementation flaw of IQueryProviderWithOptions.WithOptions method in the DefaultQueryProvider class which requires some ugly reflection based hacks. But without that the query provider will be replaced with the default if the query is using some of the WithOptions extension methods, thus negating all our efforts.

    With that being said, here is the provider code:

    public class CustomQueryProvider : DefaultQueryProvider, IQueryProviderWithOptions
    {
        // Required constructors
        public CustomQueryProvider(ISessionImplementor session) : base(session) { }
        public CustomQueryProvider(ISessionImplementor session, object collection) : base(session, collection) { }
        // The code we need
        protected override NhLinqExpression PrepareQuery(Expression expression, out IQuery query)
            => base.PrepareQuery(expression.ApplySpecifications(), out query);
        // Hacks for correctly supporting IQueryProviderWithOptions
        IQueryProvider IQueryProviderWithOptions.WithOptions(Action<NhQueryableOptions> setOptions)
        {
            if (setOptions == null)
                throw new ArgumentNullException(nameof(setOptions));
            var options = (NhQueryableOptions)_options.GetValue(this);
            var newOptions = options != null ? (NhQueryableOptions)CloneOptions.Invoke(options, null) : new NhQueryableOptions();
            setOptions(newOptions);
            var clone = (CustomQueryProvider)this.MemberwiseClone();
            _options.SetValue(clone, newOptions);
            return clone;
        }
        static readonly FieldInfo _options = typeof(DefaultQueryProvider).GetField("_options", BindingFlags.NonPublic | BindingFlags.Instance);
        static readonly MethodInfo CloneOptions = typeof(NhQueryableOptions).GetMethod("Clone", BindingFlags.NonPublic | BindingFlags.Instance);
    }
    

    The classes LinqToHqlGeneratorsRegistry and SpecificationHqlGenerator are not needed anymore, so remove them and replace

    cfg.LinqToHqlGeneratorsRegistry<LinqToHqlGeneratorsRegistry>();
    

    with

    cfg.LinqQueryProvider<CustomQueryProvider>();
    

    and everything will work as expected.