自定义 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
使用常量文字。
你想要实现的基本上是用另一个内部表达式树替换一个表达式,这正是 ExpressionVisitor
s 的目的。您所需要的只是能够在查询提供程序之前预处理查询表达式。
奇怪的是,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 LinqToHqlGeneratorsRegistry
和 SpecificationHqlGenerator
,所以删除它们并替换
cfg.LinqToHqlGeneratorsRegistry<LinqToHqlGeneratorsRegistry>();
和
cfg.LinqQueryProvider<CustomQueryProvider>();
一切都会按预期进行。
我自己实现了 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
使用常量文字。
你想要实现的基本上是用另一个内部表达式树替换一个表达式,这正是 ExpressionVisitor
s 的目的。您所需要的只是能够在查询提供程序之前预处理查询表达式。
奇怪的是,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 LinqToHqlGeneratorsRegistry
和 SpecificationHqlGenerator
,所以删除它们并替换
cfg.LinqToHqlGeneratorsRegistry<LinqToHqlGeneratorsRegistry>();
和
cfg.LinqQueryProvider<CustomQueryProvider>();
一切都会按预期进行。