自定义 LinqToHqlGeneratorsRegistry - InvalidCastException:'无法将 "Antlr.Runtime.Tree.CommonTree" 转换为 "NHibernate.Hql.Ast.ANTLR.Tree.IASTNode"

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

我自己实现了 LinqToHqlGeneratorsRegistry,以便在我的模型中使用规范模式。我可以对对象和查询使用规范,并且不重复代码(参见示例)。您可以看到所有代码here。除了一种情况外,我的代码适用于所有情况。如果规范包含 DateTime 变量,我得到 InvalidCastException。

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

异常

   в 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

为什么指定任何其他类型的变量都可以正常工作?

具体问题不是 DateTime 类型,而是 DateTime.Today 方法。

普遍的问题是在 NHibernate LINQ 查询表达式处理管道中调用 HqlGenerators 的时间太晚,因此缺少原始表达式预处理的许多部分,如部分评估、参数化等。即使使用 "working" 查询也可以很容易地看出差异 - 如果您在 LINQ 查询中直接使用 x => x.Sex == "Male",则 SQL 查询是参数化的,而翻译后的 SQL 来自 x => x.IsMale 使用常量文字。

你想要实现的基本上是用另一个内部表达式树替换一个表达式,这正是 ExpressionVisitors 的目的。您所需要的只是能够在查询提供程序之前预处理查询表达式。

奇怪的是,none 的主要 LINQ 查询提供程序(NHibernate、EF6、EF Core)提供了一种方法来执行此操作。但稍后会详细介绍。让我先展示应用规范所需的方法(省略错误检查):

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);
        }
    }
}

它使用以下助手:

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);
    }
}

现在是管道部分。实际上 NHibernate 允许您用自己的提供程序替换 LINQ 提供程序。理论上,您应该能够创建 DefaultQueryProvider 派生 class,覆盖 PrepareQuery 方法并在调用基本实现之前预处理传递的表达式。

不幸的是,DefaultQueryProvider class 中的 IQueryProviderWithOptions.WithOptions 方法存在一个实现缺陷,需要一些基于反射的丑陋黑客攻击。但是,如果查询使用某些 WithOptions 扩展方法,则查询提供程序将被默认替换,从而否定我们所有的努力。

话虽如此,这里是提供商代码:

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);
}

不再需要 classes LinqToHqlGeneratorsRegistrySpecificationHqlGenerator,所以删除它们并替换

cfg.LinqToHqlGeneratorsRegistry<LinqToHqlGeneratorsRegistry>();

cfg.LinqQueryProvider<CustomQueryProvider>();

一切都会按预期进行。