diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..8b9e94414 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root=true + +[*] +insert_final_newline = true + +[*.cs] +indent_style = space +indent_size = 2 diff --git a/Core/Parsing/ExpressionVisitors/PartialEvaluatingExpressionVisitor.cs b/Core/Parsing/ExpressionVisitors/PartialEvaluatingExpressionVisitor.cs index f7d233505..171e4d08e 100644 --- a/Core/Parsing/ExpressionVisitors/PartialEvaluatingExpressionVisitor.cs +++ b/Core/Parsing/ExpressionVisitors/PartialEvaluatingExpressionVisitor.cs @@ -59,8 +59,8 @@ private PartialEvaluatingExpressionVisitor ( PartialEvaluationInfo partialEvaluationInfo, IEvaluatableExpressionFilter evaluatableExpressionFilter) { - ArgumentUtility.CheckNotNull ("partialEvaluationInfo", partialEvaluationInfo); - ArgumentUtility.CheckNotNull ("evaluatableExpressionFilter", evaluatableExpressionFilter); + ArgumentUtility.CheckNotNull("partialEvaluationInfo", partialEvaluationInfo); + ArgumentUtility.CheckNotNull("evaluatableExpressionFilter", evaluatableExpressionFilter); _partialEvaluationInfo = partialEvaluationInfo; _evaluatableExpressionFilter = evaluatableExpressionFilter; @@ -73,7 +73,10 @@ public override Expression Visit (Expression expression) if (expression == null) return null; - if (expression.NodeType == ExpressionType.Lambda || !_partialEvaluationInfo.IsEvaluatableExpression (expression)) + if (expression.NodeType == ExpressionType.Lambda || !_partialEvaluationInfo.IsEvaluatableExpression (expression) + // inserted after allowing some parameters to be evaluatable; parameters should not be evaluated separately, but + // only as part of the method invoking lambda definining them + && expression.NodeType != ExpressionType.Parameter) return base.Visit (expression); Expression evaluatedExpression; @@ -91,7 +94,7 @@ public override Expression Visit (Expression expression) if (evaluatedExpression != expression) return EvaluateIndependentSubtrees (evaluatedExpression, _evaluatableExpressionFilter); - + return evaluatedExpression; } @@ -106,6 +109,11 @@ private Expression EvaluateSubtree (Expression subtree) { ArgumentUtility.CheckNotNull ("subtree", subtree); + // inserted after allowing some parameters to be evaluatable; parameters should not be evaluated separately, but + // only as part of the method invoking lambda definining them + if (subtree.NodeType == ExpressionType.Parameter) + return subtree; + if (subtree.NodeType == ExpressionType.Constant) { var constantExpression = (ConstantExpression) subtree; @@ -120,7 +128,7 @@ private Expression EvaluateSubtree (Expression subtree) Expression> lambdaWithoutParameters = Expression.Lambda> (Expression.Convert (subtree, typeof (object))); var compiledLambda = lambdaWithoutParameters.Compile(); - object value = compiledLambda (); + var value = compiledLambda (); return Expression.Constant (value, subtree.Type); } } diff --git a/Core/Parsing/ExpressionVisitors/TreeEvaluation/EvaluatableExpressionFilterBase.cs b/Core/Parsing/ExpressionVisitors/TreeEvaluation/EvaluatableExpressionFilterBase.cs index 4c205361a..c53646a86 100644 --- a/Core/Parsing/ExpressionVisitors/TreeEvaluation/EvaluatableExpressionFilterBase.cs +++ b/Core/Parsing/ExpressionVisitors/TreeEvaluation/EvaluatableExpressionFilterBase.cs @@ -32,6 +32,11 @@ protected EvaluatableExpressionFilterBase () { } + /// + /// Historically all parameters were considered non-evaluatable, this property allows to change this behavior, default is false. + /// + protected bool AllowParameterEvaluation { get; set; } + public virtual bool IsEvaluatableBinary (BinaryExpression node) { ArgumentUtility.CheckNotNull ("node", node); @@ -151,6 +156,25 @@ public virtual bool IsEvaluatableUnary (UnaryExpression node) return true; } + /// + /// Historically all parameters were considered non-evaluatable, the behavior remains default that can be overridden. + /// + /// + /// Expression being tested. + /// + /// + /// Lambda expression defining parameter. + /// + /// + /// + /// + public virtual bool IsEvaluatableParameter (ParameterExpression node, LambdaExpression definingLambdaExpression) + { + ArgumentUtility.CheckNotNull (nameof(node), node); + + return AllowParameterEvaluation; + } + #if !NET_3_5 public virtual bool IsEvaluatableBlock (BlockExpression node) { @@ -237,4 +261,4 @@ public virtual bool IsEvaluatableTry (TryExpression node) } #endif } -} \ No newline at end of file +} diff --git a/Core/Parsing/ExpressionVisitors/TreeEvaluation/EvaluatableTreeFindingExpressionVisitor.cs b/Core/Parsing/ExpressionVisitors/TreeEvaluation/EvaluatableTreeFindingExpressionVisitor.cs index c58344ccd..2e0a78d8c 100644 --- a/Core/Parsing/ExpressionVisitors/TreeEvaluation/EvaluatableTreeFindingExpressionVisitor.cs +++ b/Core/Parsing/ExpressionVisitors/TreeEvaluation/EvaluatableTreeFindingExpressionVisitor.cs @@ -16,6 +16,7 @@ // using System; +using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -49,6 +50,19 @@ namespace Remotion.Linq.Parsing.ExpressionVisitors.TreeEvaluation /// public sealed class EvaluatableTreeFindingExpressionVisitor : RelinqExpressionVisitor, IPartialEvaluationExceptionExpressionVisitor { + private class ParameterStatus + { + public ParameterExpression Expression { get; set; } + + public bool IsEvaluatable { get; set; } + + public LambdaExpression OwningExpression { get; set; } + + public MethodCallExpression MethodCallInvokingLambda { get; set; } + + public Expression MethodArgumentAcceptingLambda { get; set; } + } + public static PartialEvaluationInfo Analyze ( [NotNull] Expression expressionTree, [NotNull] IEvaluatableExpressionFilter evaluatableExpressionFilter) @@ -70,6 +84,9 @@ public static PartialEvaluationInfo Analyze ( private readonly PartialEvaluationInfo _partialEvaluationInfo = new PartialEvaluationInfo(); private bool _isCurrentSubtreeEvaluatable; + private readonly Stack _ancestors = new Stack(20); + private readonly Dictionary _parameters = new Dictionary(); + private EvaluatableTreeFindingExpressionVisitor (IEvaluatableExpressionFilter evaluatableExpressionFilter) { _evaluatableExpressionFilter = evaluatableExpressionFilter; @@ -77,6 +94,8 @@ private EvaluatableTreeFindingExpressionVisitor (IEvaluatableExpressionFilter ev public override Expression Visit (Expression expression) { + _ancestors.Push(expression); + if (expression == null) return base.Visit ((Expression) null); @@ -103,6 +122,17 @@ public override Expression Visit (Expression expression) // - it was evaluatable before, and // - the current subtree (i.e. the child of the parent node) is evaluatable. _isCurrentSubtreeEvaluatable &= isParentNodeEvaluatable; // the _isCurrentSubtreeEvaluatable flag now relates to the parent node again + + _ancestors.Pop(); + + if (expression?.NodeType == ExpressionType.Lambda) + { + // defined parameters go out of scope; chained extension methods often use the same parameter names + var parameterNames = _parameters.Where(p => p.Value.OwningExpression == expression).Select(p => p.Key).ToArray(); + foreach (var name in parameterNames) + _parameters.Remove(name); + } + return visitedExpression; } @@ -256,17 +286,9 @@ protected override Expression VisitMethodCall (MethodCallExpression expression) { ArgumentUtility.CheckNotNull ("expression", expression); - // Method calls are only evaluatable if they do not involve IQueryable objects. - - if (IsQueryableExpression (expression.Object)) + if (!IsEvaluatableMethodCall(expression)) _isCurrentSubtreeEvaluatable = false; - for (int i = 0; i < expression.Arguments.Count && _isCurrentSubtreeEvaluatable; i++) - { - if (IsQueryableExpression (expression.Arguments[i])) - _isCurrentSubtreeEvaluatable = false; - } - var vistedExpression = base.VisitMethodCall (expression); // Testing the parent expression is only required if all children are evaluatable @@ -326,9 +348,22 @@ protected override Expression VisitParameter (ParameterExpression expression) { ArgumentUtility.CheckNotNull ("expression", expression); - // Parameters are not evaluatable. - _isCurrentSubtreeEvaluatable = false; - return base.VisitParameter (expression); + // Parameters are evaluatable if they are supplied by evaluatable expression. + // look up lambda defining the parameter, up the ancestor list and check if method call to which it is passed is on evaluatable instance of extension method accepting evaluatable arguments + // note that lambda body is visited prior to parameters + // since method call is visited in the order {instance, arguments} and extension methods get instance as first parameter + // the source of the parameter is already visited and its evaluatability established + var status = GetParameterStatus (expression); + if (!status.IsEvaluatable) + _isCurrentSubtreeEvaluatable = false; + + var visitedExpression = base.VisitParameter(expression); + + // default filter will prevent evaluation of expressions involving parameters for backward compatibility, but it can be overridden + if (_isCurrentSubtreeEvaluatable) + _isCurrentSubtreeEvaluatable = _evaluatableExpressionFilter.IsEvaluatableParameter(expression, status.OwningExpression); + + return visitedExpression; } protected override Expression VisitNewArray (NewArrayExpression expression) @@ -631,9 +666,9 @@ private bool IsCurrentExpressionEvaluatable (Expression expression) } #endif - private bool IsQueryableExpression (Expression expression) + private static bool IsQueryableExpression(Expression expression) { - return expression != null && typeof (IQueryable).GetTypeInfo().IsAssignableFrom (expression.Type.GetTypeInfo()); + return expression != null && typeof(IQueryable).GetTypeInfo().IsAssignableFrom(expression.Type.GetTypeInfo()); } public Expression VisitPartialEvaluationException (PartialEvaluationExceptionExpression partialEvaluationExceptionExpression) @@ -644,5 +679,120 @@ public Expression VisitPartialEvaluationException (PartialEvaluationExceptionExp _isCurrentSubtreeEvaluatable = false; return partialEvaluationExceptionExpression; } + + /// + /// Crude implementation, only direct checks of queryable instance and arguments. + /// + /// + /// false if instance (on which method is invoked) or any of its arguments implements . + /// + public static bool IsEvaluatableMethodCall(MethodCallExpression expression) + { + ArgumentUtility.CheckNotNull (nameof(expression), expression); + + // Method calls are only evaluatable if they do not involve IQueryable objects. + + return !IsQueryableExpression(expression.Object) + && expression.Arguments.All(a => !IsQueryableExpression(a)); + } + + private ParameterStatus GetParameterStatus(ParameterExpression expression) + { + // consider nameless parameters as non-evaluatable as their handling is unclear at the time of writing + if (expression?.Name == null) + return new ParameterStatus() {Expression = expression, IsEvaluatable = false}; + + ParameterStatus status; + if (!_parameters.TryGetValue(expression.Name, out status)) + { + status = CalcParameterStatus(expression); + _parameters.Add(expression.Name, status); + } + return status; + } + + /// + /// Parameters are evaluatable if they are supplied by evaluatable expression. + /// Look up lambda defining the parameter, up the ancestor list and check if method call to which it is passed is on evaluatable + /// instance or extension method accepting evaluatable arguments. + /// Note that lambda body is visited prior to parameters. + /// Since method call is visited in the order [instance, arguments] and extension methods get instance as first parameter + /// the source of the parameter is already visited and its evaluatability established. + /// Note that if parameter is evaluated method call must be evaluatad too eliminating lambda and all the parameters. + /// If lambda is not eliminated, evaluated parameter produces an error complaining that evaluating lambda parameter + /// must return non-null expression of the same type. + /// + private ParameterStatus CalcParameterStatus(ParameterExpression expression) + { + ArgumentUtility.CheckNotNull (nameof(expression), expression); + + var result = new ParameterStatus { Expression = expression }; + + foreach (var ancestor in _ancestors.Where(x => x != null)) + { + if (result.MethodArgumentAcceptingLambda == null && IsParameterOwner(ancestor, expression)) + { + result.MethodArgumentAcceptingLambda = result.OwningExpression = (LambdaExpression)ancestor; + } + else if (result.MethodArgumentAcceptingLambda != null) + { + if (ancestor.NodeType == ExpressionType.Call) + { + result.MethodCallInvokingLambda = (MethodCallExpression)ancestor; + + result.IsEvaluatable = IsMethodSupplyingEvaluatableParameterValues(result.MethodCallInvokingLambda, result.MethodArgumentAcceptingLambda); + + return result; + } + + result.MethodArgumentAcceptingLambda = ancestor; + } + } + + return result; + } + + /// + /// Can be used to determine whether parameters defined by lambda expression are evaluatable by examining method which will + /// supply parameters to it. Relies on the visiting sequence: lambda parameters are visited after its body. When parameter + /// is visited, the status of the defining lambda and method call supplying parameters to it has not been established and + /// would not contain them. There's also a + /// chicken-and-egg situation as parameter is visited as a child of lambda body (potentially deep in the tree), but its + /// evaluatability is determined at the level above lambda. To solve this here we examine the method that accepts the + /// lambda as one of its parameters and determine if all other arguments are evaluatable, expecting all of them to be + /// already visited and present in . + /// + /// + /// Method invoking lambda. + /// + /// + /// Argument (quote) for the lambda expression to which the method passes parameter values. + /// + /// + /// Member expressions on e.g. repository creating query instances must be evaluated into constants + /// and queryable constants must be evaluated (e.g. for subqueries to be expanded properly), + /// but parameters accepting values from them are not evaluatable. + /// + private bool IsMethodSupplyingEvaluatableParameterValues(MethodCallExpression methodExpression, Expression methodArgumentAcceptingLambda) + { + ArgumentUtility.CheckNotNull (nameof(methodExpression), methodExpression); + ArgumentUtility.CheckNotNull (nameof(methodArgumentAcceptingLambda), methodArgumentAcceptingLambda); + + if (!IsEvaluatableMethodCall(methodExpression)) + return false; + + if (methodExpression.Object != null && !_partialEvaluationInfo.IsEvaluatableExpression(methodExpression.Object)) + return false; + + return methodExpression.Arguments.All(a => a == methodArgumentAcceptingLambda || _partialEvaluationInfo.IsEvaluatableExpression(a)); + } + + private bool IsParameterOwner(Expression expression, ParameterExpression parameterExpression) + { + ArgumentUtility.CheckNotNull (nameof(expression), expression); + ArgumentUtility.CheckNotNull (nameof(parameterExpression), parameterExpression); + + return (expression as LambdaExpression)?.Parameters.Any(x => x.Name == parameterExpression.Name) == true; + } } -} \ No newline at end of file +} diff --git a/Core/Parsing/ExpressionVisitors/TreeEvaluation/IEvaluatableExpressionFilter.cs b/Core/Parsing/ExpressionVisitors/TreeEvaluation/IEvaluatableExpressionFilter.cs index 11504b6c8..d08696b39 100644 --- a/Core/Parsing/ExpressionVisitors/TreeEvaluation/IEvaluatableExpressionFilter.cs +++ b/Core/Parsing/ExpressionVisitors/TreeEvaluation/IEvaluatableExpressionFilter.cs @@ -55,6 +55,15 @@ public interface IEvaluatableExpressionFilter bool IsEvaluatableNewArray ([NotNull] NewArrayExpression node); bool IsEvaluatableTypeBinary ([NotNull] TypeBinaryExpression node); bool IsEvaluatableUnary ([NotNull] UnaryExpression node); + + /// + /// Expression being tested. + /// + /// + /// Lambda expression defining parameter. Null for nameless parameters. + /// + bool IsEvaluatableParameter ([NotNull] ParameterExpression node, LambdaExpression definingLambdaExpression); + #if !NET_3_5 bool IsEvaluatableBlock ([NotNull] BlockExpression node); bool IsEvaluatableCatchBlock ([NotNull] CatchBlock node); @@ -70,4 +79,4 @@ public interface IEvaluatableExpressionFilter bool IsEvaluatableTry ([NotNull] TryExpression node); #endif } -} \ No newline at end of file +} diff --git a/Core/Parsing/ExpressionVisitors/TreeEvaluation/NullEvaluatableExpressionFilter.cs b/Core/Parsing/ExpressionVisitors/TreeEvaluation/NullEvaluatableExpressionFilter.cs index aa19d423b..155f0b79e 100644 --- a/Core/Parsing/ExpressionVisitors/TreeEvaluation/NullEvaluatableExpressionFilter.cs +++ b/Core/Parsing/ExpressionVisitors/TreeEvaluation/NullEvaluatableExpressionFilter.cs @@ -28,4 +28,4 @@ public NullEvaluatableExpressionFilter () { } } -} \ No newline at end of file +} diff --git a/Core/Parsing/Structure/QueryParser.cs b/Core/Parsing/Structure/QueryParser.cs index be41729ee..5371ea2b1 100644 --- a/Core/Parsing/Structure/QueryParser.cs +++ b/Core/Parsing/Structure/QueryParser.cs @@ -41,13 +41,17 @@ public sealed class QueryParser : IQueryParser /// , and /// for details. /// - public static QueryParser CreateDefault () + /// + /// Filter allowing to disable evaluation of independent subtrees even if they can potentially be evaluated; optional, + /// is used by default. + /// + public static QueryParser CreateDefault (IEvaluatableExpressionFilter evaluatableExpressionFilter = null) { var transformerRegistry = ExpressionTransformerRegistry.CreateDefault(); - var evaluatableExpressionFilter = new NullEvaluatableExpressionFilter(); + var ensuredFilter = evaluatableExpressionFilter ?? new NullEvaluatableExpressionFilter(); var expressionTreeParser = new ExpressionTreeParser ( ExpressionTreeParser.CreateDefaultNodeTypeProvider (), - ExpressionTreeParser.CreateDefaultProcessor (transformerRegistry, evaluatableExpressionFilter)); + ExpressionTreeParser.CreateDefaultProcessor (transformerRegistry, ensuredFilter)); return new QueryParser(expressionTreeParser); } diff --git a/Development/UnitTesting/Parsing/TestEvaluatableExpressionFilter.cs b/Development/UnitTesting/Parsing/TestEvaluatableExpressionFilter.cs index 630df50a8..c745d125c 100644 --- a/Development/UnitTesting/Parsing/TestEvaluatableExpressionFilter.cs +++ b/Development/UnitTesting/Parsing/TestEvaluatableExpressionFilter.cs @@ -26,8 +26,9 @@ namespace Remotion.Linq.Development.UnitTesting.Parsing /// public sealed class TestEvaluatableExpressionFilter : EvaluatableExpressionFilterBase { - public TestEvaluatableExpressionFilter () + public TestEvaluatableExpressionFilter (bool allowParameterEvaluation = false) { + base.AllowParameterEvaluation = allowParameterEvaluation; } } -} \ No newline at end of file +} diff --git a/Relinq.sln.DotSettings b/Relinq.sln.DotSettings index 56016a6db..4e4e75d4b 100644 --- a/Relinq.sln.DotSettings +++ b/Relinq.sln.DotSettings @@ -40,6 +40,8 @@ False False + NEVER + NEVER LINE_BREAK True True @@ -145,7 +147,11 @@ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + True + True + True True + True True True True diff --git a/UnitTests/Parsing/ExpressionVisitors/PartialEvaluatingExpressionVisitorTest.cs b/UnitTests/Parsing/ExpressionVisitors/PartialEvaluatingExpressionVisitorTest.cs index 17cc97a03..a6e296953 100644 --- a/UnitTests/Parsing/ExpressionVisitors/PartialEvaluatingExpressionVisitorTest.cs +++ b/UnitTests/Parsing/ExpressionVisitors/PartialEvaluatingExpressionVisitorTest.cs @@ -109,6 +109,31 @@ public void EvaluateLambdaWithSubQuery () Assert.That (result, Is.SameAs (lambdaExpression)); } + [Test] + public void EvaluateSelectOnConstant() + { + var restaurants = new[] { new Restaurant {ID = 1, SubKitchen = new Kitchen { ID = 2, Name = "R Kitchen", RoomNumber = 12 } } }; + + Expression> selectPredicate = x => restaurants.Select(y => y.SubKitchen).Contains(x.Kitchen); + + var result = PartialEvaluatingExpressionVisitor.EvaluateIndependentSubtrees(selectPredicate, new TestEvaluatableExpressionFilter(true)); + Assert.That(result, Is.Not.SameAs(selectPredicate)); + Assert.IsInstanceOf(result); + Assert.IsInstanceOf(((LambdaExpression)result).Body); + + var rootResult = (MethodCallExpression)((LambdaExpression)result).Body; + + Assert.AreEqual (typeof (bool), rootResult.Method.ReturnType); + Assert.AreEqual(typeof(Enumerable), rootResult.Method.DeclaringType); + Assert.AreEqual(nameof(Enumerable.Contains), rootResult.Method.Name); + Assert.AreEqual (2, rootResult.Arguments.Count); + + var firstArgument = rootResult.Arguments[0]; + + Assert.AreEqual(ExpressionType.Constant, firstArgument.NodeType); + Assert.IsInstanceOf>(((ConstantExpression)firstArgument).Value); + } + [Test] public void EvaluateWholeQueryTree () { diff --git a/UnitTests/Parsing/Structure/QueryParserIntegrationTests/SubQueryQueryParserIntegrationTest.cs b/UnitTests/Parsing/Structure/QueryParserIntegrationTests/SubQueryQueryParserIntegrationTest.cs index 44426f540..9c8609f2a 100644 --- a/UnitTests/Parsing/Structure/QueryParserIntegrationTests/SubQueryQueryParserIntegrationTest.cs +++ b/UnitTests/Parsing/Structure/QueryParserIntegrationTests/SubQueryQueryParserIntegrationTest.cs @@ -23,6 +23,8 @@ using Remotion.Linq.Clauses.Expressions; using Remotion.Linq.Clauses.ResultOperators; using Remotion.Linq.Development.UnitTesting; +using Remotion.Linq.Development.UnitTesting.Parsing; +using Remotion.Linq.Parsing.Structure; using Remotion.Linq.UnitTests.TestDomain; using Remotion.Linq.UnitTests.TestQueryGenerators; @@ -74,7 +76,45 @@ public void SubQuery_InNewExpression_RetainsType () Assert.That (((NewExpression) queryModel.SelectClause.Selector).Arguments[0].Type, Is.SameAs (typeof (IEnumerable))); var subQueryModel = ((SubQueryExpression) ((NewExpression) queryModel.SelectClause.Selector).Arguments[0]).QueryModel; - Assert.That (subQueryModel.GetOutputDataInfo ().DataType, Is.SameAs (typeof (IEnumerable))); + Assert.That (subQueryModel.GetOutputDataInfo ().DataType, Is.SameAs (typeof(IEnumerable))); + } + + [Test] + public void SubQuery_InNewExpression_RetainsTypeWithEvaluatableParameterButQueryableConstant() + { + var queryable = from s in QuerySource select new { Result = from s2 in QuerySource select s2 }; + var parser = QueryParser.CreateDefault(new TestEvaluatableExpressionFilter(true)); + var queryModel = parser.GetParsedQuery(queryable.Expression); + + Assert.That(queryModel.SelectClause.Selector, Is.TypeOf(typeof(NewExpression))); + Assert.That(((NewExpression)queryModel.SelectClause.Selector).Arguments[0], Is.TypeOf(typeof(SubQueryExpression))); + Assert.That(((NewExpression)queryModel.SelectClause.Selector).Arguments[0].Type, Is.SameAs(typeof(IQueryable))); + + var subQueryModel = ((SubQueryExpression)((NewExpression)queryModel.SelectClause.Selector).Arguments[0]).QueryModel; + Assert.That(subQueryModel.GetOutputDataInfo().DataType, Is.SameAs(typeof(IQueryable))); + } + + /// + /// Casting queriable to IEnumerable executes the query and produces constant from the point of view of e.g. database LINQ provider. + /// In such cases evaluatable expression filter has to be instructed to allow evaluation of expressions involving parameters. + /// + [Test] + public void SubQuery_InNewExpression_ConvertsToConstantWithEvaluatableParameter() + { + var queryable = from s in QuerySource select new { Result = from s2 in (IEnumerable)QuerySource select s2 }; + var parser = QueryParser.CreateDefault (new TestEvaluatableExpressionFilter (true)); + var queryModel = parser.GetParsedQuery(queryable.Expression); + + Assert.That(queryModel.SelectClause.Selector, Is.TypeOf(typeof(ConstantExpression))); + var constantExpression = (ConstantExpression)queryModel.SelectClause.Selector; + + var resultProperty = constantExpression.Type.GetProperty ("Result"); + Assert.That (resultProperty, Is.Not.Null); + + var resultValue = resultProperty.GetValue (constantExpression.Value, null); + Assert.That(resultValue, Is.Not.Null); + + Assert.That(resultValue, Is.AssignableTo(typeof(IEnumerable))); } [Test]