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

时间:2019-05-14 15:38:00

标签: c# nhibernate expression-trees

我暗含了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

为什么带有任何其他类型变量的规范都能正常工作?

1 个答案:

答案 0 :(得分:2)

特殊的问题不是DateTime类型,而是DateTime.Today方法。

一般的问题是HqlGenerators在NHibernate LINQ查询表达式处理管道中被调用为时已晚,因此原始表达式预处理的许多部分(如部分求值,参数化等)都丢失了。即使使用“工作”查询,也可以轻松看出差异-如果直接在LINQ查询中使用x => x.Sex == "Male",则将对SQL查询进行参数化,而从x => x.IsMale转换过来的SQL将使用常量文字。

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

奇怪的是,没有主要的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派生类,重写PrepareQuery方法并在调用基本实现之前对传递的表达式进行预处理。

不幸的是,IQueryProviderWithOptions.WithOptions类中的DefaultQueryProvider方法存在一个实现缺陷,需要一些基于丑陋反射的技巧。但是如果查询使用的是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);
}

不再需要类LinqToHqlGeneratorsRegistrySpecificationHqlGenerator,因此请删除并替换

cfg.LinqToHqlGeneratorsRegistry<LinqToHqlGeneratorsRegistry>();

使用

cfg.LinqQueryProvider<CustomQueryProvider>();

一切都会按预期进行。