Пользовательское LinqToHqlGeneratorsRegistry - InvalidCastException: «Невозможно привести« Antlr.Runtime.Tree.CommonTree »к« NHibernate.Hql.Ast.ANTLR.Tree.IASTNode » - PullRequest
1 голос

Я делаю собственную реализацию LinqToHqlGeneratorsRegistry для использования шаблона спецификации в моих моделях. Я могу использовать спецификацию с объектами и запросами и не повторять код (см. Пример). Вы можете увидеть весь код здесь . Мой код прекрасно работает во всех случаях, кроме одного. Я получил InvalidCastException, если спецификация содержит переменную DateTime.

    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 Ответ

2 голосов
/ 21 мая 2019

Особая проблема не в типе DateTime, а в методе DateTime.Today.

Общая проблема заключается в том, что HqlGenerators вызываются слишком поздно в конвейере обработки выражения запроса NHibernate LINQ, так много частейпервоначальной предварительной обработки выражения, такой как частичная оценка, параметризация и т. д.Разницу можно легко увидеть даже с помощью «рабочего» запроса: если вы используете x => x.Sex == "Male" непосредственно в запросе LINQ, запрос SQL параметризуется, а в переведенном SQL из x => x.IsMale используется константный литерал.

То, чего вы пытаетесь достичь, - это в основном замена одного выражения другим внутри дерева выражений, для чего и нужны ExpressionVisitor s.И все, что вам нужно, - это иметь возможность предварительно обработать выражение запроса перед поставщиком запросов.

Как ни странно, ни один из основных поставщиков запросов 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);
}

Классы LinqToHqlGeneratorsRegistry и SpecificationHqlGenerator больше не нужны, поэтому удалите их и замените

cfg.LinqToHqlGeneratorsRegistry<LinqToHqlGeneratorsRegistry>();

на

cfg.LinqQueryProvider<CustomQueryProvider>();

и все будет работатькак и ожидалось.

...