diff --git a/EFCore.sln.DotSettings b/EFCore.sln.DotSettings index 1b4974fd608..d1ddc281f56 100644 --- a/EFCore.sln.DotSettings +++ b/EFCore.sln.DotSettings @@ -291,14 +291,21 @@ The .NET Foundation licenses this file to you under the MIT license. True True True + True + True + True True True True True True + True True + True + True True True + True True True True @@ -316,6 +323,7 @@ The .NET Foundation licenses this file to you under the MIT license. True True True + True True True True diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 1eb205020bb..4678648bc8d 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -982,8 +982,8 @@ public static string EFConstantInvoked /// /// The EF.Constant<T> method may only be used with an argument that can be evaluated client-side and does not contain any reference to database-side entities. /// - public static string EFConstantWithNonEvaluableArgument - => GetString("EFConstantWithNonEvaluableArgument"); + public static string EFConstantWithNonEvaluatableArgument + => GetString("EFConstantWithNonEvaluatableArgument"); /// /// The EF.Parameter<T> method may only be used within Entity Framework LINQ queries. @@ -991,6 +991,12 @@ public static string EFConstantWithNonEvaluableArgument public static string EFParameterInvoked => GetString("EFParameterInvoked"); + /// + /// The EF.Parameter<T> method may only be used with an argument that can be evaluated client-side and does not contain any reference to database-side entities. + /// + public static string EFParameterWithNonEvaluatableArgument + => GetString("EFParameterWithNonEvaluatableArgument"); + /// /// Complex type '{complexType}' has no properties defines. Configure at least one property or don't include this type in the model. /// @@ -1766,6 +1772,18 @@ public static string ManyToManyOneNav(object? entityType, object? navigation) GetString("ManyToManyOneNav", nameof(entityType), nameof(navigation)), entityType, navigation); + /// + /// EF Core does not support MemberListBinding: 'new Blog { Posts = { new Post(), new Post() } }'. + /// + public static string MemberListBindingNotSupported + => GetString("MemberListBindingNotSupported"); + + /// + /// EF Core does not support MemberMemberBinding: 'new Blog { Data = { Name = "hello world" } }'. + /// + public static string MemberMemberBindingNotSupported + => GetString("MemberMemberBindingNotSupported"); + /// /// The specified field '{field}' could not be found for property '{2_entityType}.{1_property}'. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 9d7fe91b5f7..7ad9abf40d8 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -480,12 +480,15 @@ The EF.Constant<T> method may only be used within Entity Framework LINQ queries. - + The EF.Constant<T> method may only be used with an argument that can be evaluated client-side and does not contain any reference to database-side entities. The EF.Parameter<T> method may only be used within Entity Framework LINQ queries. + + The EF.Parameter<T> method may only be used with an argument that can be evaluated client-side and does not contain any reference to database-side entities. + Complex type '{complexType}' has no properties defines. Configure at least one property or don't include this type in the model. @@ -1108,6 +1111,12 @@ The navigation '{entityType}.{navigation}' cannot be used for both sides of a many-to-many relationship. Many-to-many relationships must use two distinct navigation properties. + + EF Core does not support MemberListBinding: 'new Blog { Posts = { new Post(), new Post() } }'. + + + EF Core does not support MemberMemberBinding: 'new Blog { Data = { Name = "hello world" } }'. + The specified field '{field}' could not be found for property '{2_entityType}.{1_property}'. diff --git a/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs b/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs new file mode 100644 index 00000000000..6e1427b3d54 --- /dev/null +++ b/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs @@ -0,0 +1,2029 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using E = System.Linq.Expressions.Expression; + +namespace Microsoft.EntityFrameworkCore.Query.Internal; + +/// +/// This visitor identifies subtrees in the query which can be evaluated client-side (i.e. no reference to server-side resources), +/// and evaluates those subtrees, integrating the result either as a constant (if the subtree contained no captured closure variables), +/// or as parameters. +/// +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public class ExpressionTreeFuncletizer : ExpressionVisitor +{ + // The general algorithm here is the following. + // 1. First, for each node type, visit that node's children and get their states (evaluatable, contains evaluatable, no evaluatable). + // 2. Calculate the parent node's aggregate state from its children; a container node whose children are all evaluatable is itself + // evaluatable, etc. + // 3. If the parent node is evaluatable (because all its children are), simply bubble that up - nothing more to do + // 4. If the parent node isn't evaluatable but contains an evaluatable child, that child is an evaluatable root for its fragment. + // Evaluate it, making it either into a parameter (if it contains any captured variables), or into a constant (if not). + // 5. If we're in path extraction mode (precompiled queries), build a path back up from the evaluatable roots to the query root; this + // is what later gets used to generate code to evaluate and extract those fragments as parameters. If we're in regular parameter + // parameter extraction (not precompilation), don't do this (not needed) and just return "not evaluatable". + + /// + /// Indicates whether we're calculating the paths to all parameterized evaluatable roots (precompilation mode), or doing regular, + /// non-precompiled parameter extraction. + /// + private bool _calculatingPath; + + /// + /// Indicates whether we should parameterize. Is false in in compiled query mode, as well as when we're handling query filters + /// from NavigationExpandingExpressionVisitor. + /// + private bool _parameterize; + + /// + /// Indicates whether we're currently within a lambda. When not in a lambda, we evaluate evaluatables as constants even if they + /// don't contains a captured variable (Skip/Take case). + /// + private bool _inLambda; + + /// + /// A provider-facing extensibility hook to allow preventing certain expression nodes from being evaluated (typically specific + /// methods). + /// + private readonly IEvaluatableExpressionFilter _evaluatableExpressionFilter; + + /// + /// is generally considered as non-evaluatable, since it represents a lambda parameter and we + /// don't evaluate lambdas. The one exception is a Select operator over something evaluatable (e.g. a parameterized list) - this + /// does need to get evaluated. This list contains instances for that case, to allow + /// evaluatability. + /// + private readonly HashSet _evaluatableParameters = new(); + + /// + /// A cache of tree fragments that have already been parameterized, along with their parameter. This allows us to reuse the same + /// query parameter twice when the same captured variable is referenced in the query. + /// + private readonly Dictionary _parameterizedValues = new(ExpressionEqualityComparer.Instance); + + /// + /// Used only when evaluating arbitrary QueryRootExpressions (specifically SqlQueryRootExpression), to force any evaluatable nested + /// expressions to get evaluated as roots, since the query root itself is never evaluatable. + /// + private bool _evaluateRoot; + + /// + /// Enabled only when funcletization is invoked on query filters from within NavigationExpandingExpressionVisitor. Causes special + /// handling for DbContext when it's referenced from within the query filter (e.g. for the tenant ID). + /// + private readonly bool _generateContextAccessors; + + private IQueryProvider? _currentQueryProvider; + private State _state; + private IParameterValues _parameterValues = null!; + + private readonly IModel _model; + private readonly ContextParameterReplacer _contextParameterReplacer; + private readonly IDiagnosticsLogger _logger; + + private static readonly MethodInfo ReadOnlyCollectionIndexerGetter = typeof(ReadOnlyCollection).GetProperties() + .Single(p => p.GetIndexParameters() is { Length: 1 } indexParameters && indexParameters[0].ParameterType == typeof(int)).GetMethod!; + + private static readonly MethodInfo ReadOnlyMemberBindingCollectionIndexerGetter = typeof(ReadOnlyCollection) + .GetProperties() + .Single(p => p.GetIndexParameters() is { Length: 1 } indexParameters && indexParameters[0].ParameterType == typeof(int)).GetMethod!; + + private static readonly PropertyInfo MemberAssignmentExpressionProperty = + typeof(MemberAssignment).GetProperty(nameof(MemberAssignment.Expression))!; + + private static readonly ArrayPool StateArrayPool = ArrayPool.Shared; + + private const string QueryFilterPrefix = "ef_filter"; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public ExpressionTreeFuncletizer( + IModel model, + IEvaluatableExpressionFilter evaluatableExpressionFilter, + Type contextType, + bool generateContextAccessors, + IDiagnosticsLogger logger) + { + _model = model; + _evaluatableExpressionFilter = evaluatableExpressionFilter; + _generateContextAccessors = generateContextAccessors; + _contextParameterReplacer = _generateContextAccessors + ? new ContextParameterReplacer(contextType) + : null!; + _logger = logger; + } + + /// + /// Processes an expression tree, extracting parameters and evaluating evaluatable fragments as part of the pass. + /// Used for regular query execution (neither compiled nor pre-compiled). + /// + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public E ExtractParameters( + E expression, + IParameterValues parameterValues, + bool parameterize, + bool clearParameterizedValues) + { + Reset(clearParameterizedValues); + _parameterValues = parameterValues; + _parameterize = parameterize; + _calculatingPath = false; + + var root = Visit(expression, out var state); + + Check.DebugAssert(!state.ContainsEvaluatable, "In parameter extraction mode, end state should not contain evaluatable"); + + // If the top-most node in the tree is evaluatable, evaluate it. + if (state.IsEvaluatable) + { + root = ProcessEvaluatableRoot(root, ref state); + } + + return root; + } + + /// + /// Processes an expression tree, locates references to captured variables and returns information on how to extract them from + /// expression trees with the same shape. Used to generate C# code for query precompilation. + /// + /// A tree representing the path to each evaluatable root node in the tree. + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public PathNode? CalculatePathsToEvaluatableRoots(E expression) + { + Reset(); + _calculatingPath = true; + + // In precompilation mode we don't actually extract parameter values; but we do need to generate the parameter names, using the + // same logic (and via the same code) used in parameter extraction, and that logic requires _parameterValues. + _parameterValues = new DummyParameterValues(); + + _ = Visit(expression, out var state); + + return state.Path; + } + + private void Reset(bool clearParameterizedValues = true) + { + _inLambda = false; + _currentQueryProvider = null; + _evaluateRoot = false; + _evaluatableParameters.Clear(); + + if (clearParameterizedValues) + { + _parameterizedValues.Clear(); + } + } + + [return: NotNullIfNotNull("expression")] + private E? Visit(E? expression, out State state) + { + _state = default; + var result = base.Visit(expression); + state = _state; + return result; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [return: NotNullIfNotNull("expression")] + public override E? Visit(E? expression) + { + _state = default; + + if (_evaluateRoot) + { + // This path is only called from VisitExtension for query roots, as a way of evaluating expressions inside query roots + // (i.e. SqlQueryRootExpression.Arguments). + _evaluateRoot = false; + var result = base.Visit(expression); + _evaluateRoot = true; + + if (_state.IsEvaluatable) + { + result = ProcessEvaluatableRoot(result, ref _state); + // TODO: Test this scenario in path calculation mode (probably need to handle children path?) + } + + return result; + } + + return base.Visit(expression); + } + + #region Visitation implementations + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override E VisitBinary(BinaryExpression binary) + { + var left = Visit(binary.Left, out var leftState); + + // Perform short-circuiting checks to avoid evaluating the right side if not necessary + object? leftValue = null; + if (leftState.IsEvaluatable) + { + switch (binary.NodeType) + { + case ExpressionType.Coalesce: + leftValue = Evaluate(left); + + switch (leftValue) + { + case null: + return Visit(binary.Right, out _state); + case bool b: + _state = leftState with { StateType = StateType.EvaluatableWithoutCapturedVariable }; + return E.Constant(b); + default: + return left; + } + + case ExpressionType.OrElse or ExpressionType.AndAlso when Evaluate(left) is bool leftBoolValue: + { + left = E.Constant(leftBoolValue); + leftState = leftState with { StateType = StateType.EvaluatableWithoutCapturedVariable }; + + if (leftBoolValue && binary.NodeType is ExpressionType.OrElse + || !leftBoolValue && binary.NodeType is ExpressionType.AndAlso) + { + _state = leftState; + return left; + } + + binary = binary.Update(left, binary.Conversion, binary.Right); + break; + } + } + } + + var right = Visit(binary.Right, out var rightState); + + if (binary.NodeType is ExpressionType.AndAlso or ExpressionType.OrElse) + { + if (leftState.IsEvaluatable && leftValue is bool leftBoolValue) + { + switch ((leftConstant: leftBoolValue, binary.NodeType)) + { + case (true, ExpressionType.AndAlso) or (false, ExpressionType.OrElse): + _state = rightState; + return right; + case (true, ExpressionType.OrElse) or (false, ExpressionType.AndAlso): + throw new UnreachableException(); // Already handled above before visiting the right side + } + } + + if (rightState.IsEvaluatable && Evaluate(right) is bool rightBoolValue) + { + switch ((binary.NodeType, rightConstant: rightBoolValue)) + { + case (ExpressionType.AndAlso, true) or (ExpressionType.OrElse, false): + _state = leftState; + return left; + case (ExpressionType.OrElse, true) or (ExpressionType.AndAlso, false): + _state = rightState with { StateType = StateType.EvaluatableWithoutCapturedVariable }; + return E.Constant(rightBoolValue); + } + } + } + + // We're done with simplification/short-circuiting checks specific to BinaryExpression. + var state = CombineStateTypes(leftState.StateType, rightState.StateType); + + switch (state) + { + case StateType.EvaluatableWithCapturedVariable or StateType.EvaluatableWithoutCapturedVariable + when IsGenerallyEvaluatable(binary): + _state = State.CreateEvaluatable(typeof(BinaryExpression), state is StateType.EvaluatableWithCapturedVariable); + break; + + case StateType.NoEvaluatability: + _state = State.NoEvaluatability; + break; + + default: + if (leftState.IsEvaluatable) + { + left = ProcessEvaluatableRoot(left, ref leftState); + } + + if (rightState.IsEvaluatable) + { + right = ProcessEvaluatableRoot(right, ref rightState); + } + + List? children = null; + + if (_calculatingPath) + { + if (leftState.ContainsEvaluatable) + { + children = + [ + leftState.Path! with { PathFromParent = static e => E.Property(e, nameof(BinaryExpression.Left)) } + ]; + } + + if (rightState.ContainsEvaluatable) + { + children ??= new(); + children.Add(rightState.Path! with { PathFromParent = static e => E.Property(e, nameof(BinaryExpression.Right)) }); + } + } + + _state = children is null + ? State.NoEvaluatability + : State.CreateContainsEvaluatable(typeof(ConditionalExpression), children); + break; + } + + return binary.Update(left, binary.Conversion, right); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override E VisitConditional(ConditionalExpression conditional) + { + var test = Visit(conditional.Test, out var testState); + + // If the test evaluates, simplify the conditional away by bubbling up the leg that remains + if (testState.IsEvaluatable && Evaluate(conditional.Test) is bool testBoolValue) + { + return testBoolValue + ? Visit(conditional.IfTrue, out _state) + : Visit(conditional.IfFalse, out _state); + } + + var ifTrue = Visit(conditional.IfTrue, out var ifTrueState); + var ifFalse = Visit(conditional.IfFalse, out var ifFalseState); + + var state = CombineStateTypes(testState.StateType, CombineStateTypes(ifTrueState.StateType, ifFalseState.StateType)); + + switch (state) + { + // If all three children are evaluatable, so is this conditional expression; simply bubble up, we're part of an evaluatable + // fragment that will get evaluated somewhere above. + case StateType.EvaluatableWithCapturedVariable or StateType.EvaluatableWithoutCapturedVariable + when IsGenerallyEvaluatable(conditional): + _state = State.CreateEvaluatable(typeof(ConditionalExpression), state is StateType.EvaluatableWithCapturedVariable); + break; + + case StateType.NoEvaluatability: + _state = State.NoEvaluatability; + break; + + default: + if (testState.IsEvaluatable) + { + // Early optimization - if the test is evaluatable, simply reduce the conditional to the relevant clause + if (Evaluate(test) is bool testConstant) + { + _state = testConstant ? ifTrueState : ifFalseState; + return testConstant ? ifTrue : ifFalse; + } + + test = ProcessEvaluatableRoot(test, ref testState); + } + + if (ifTrueState.IsEvaluatable) + { + ifTrue = ProcessEvaluatableRoot(ifTrue, ref ifTrueState); + } + + if (ifFalseState.IsEvaluatable) + { + ifFalse = ProcessEvaluatableRoot(ifFalse, ref ifFalseState); + } + + List? children = null; + + if (_calculatingPath) + { + if (testState.ContainsEvaluatable) + { + children ??= new(); + children.Add( + testState.Path! with { PathFromParent = static e => E.Property(e, nameof(ConditionalExpression.Test)) }); + } + + if (ifTrueState.ContainsEvaluatable) + { + children ??= new(); + children.Add( + ifTrueState.Path! with { PathFromParent = static e => E.Property(e, nameof(ConditionalExpression.IfTrue)) }); + } + + if (ifFalseState.ContainsEvaluatable) + { + children ??= new(); + children.Add( + ifFalseState.Path! with { PathFromParent = static e => E.Property(e, nameof(ConditionalExpression.IfFalse)) }); + } + } + + _state = children is null + ? State.NoEvaluatability + : State.CreateContainsEvaluatable(typeof(ConditionalExpression), children); + break; + } + + return conditional.Update(test, ifTrue, ifFalse); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override E VisitConstant(ConstantExpression constant) + { + // Whether this constant represents a captured variable determines whether we'll evaluate it as a parameter (if yes) or as a + // constant (if no). + var isCapturedVariable = + // This identifies compiler-generated closure types which contain captured variables. + (constant.Type.Attributes.HasFlag(TypeAttributes.NestedPrivate) + && Attribute.IsDefined(constant.Type, typeof(CompilerGeneratedAttribute), inherit: true)) + // The following is for supporting the Find method (we should look into this and possibly clean it up). + || constant.Type == typeof(ValueBuffer); + + _state = constant.Value is IQueryable + ? State.NoEvaluatability + : State.CreateEvaluatable(typeof(ConstantExpression), isCapturedVariable); + + return constant; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override E VisitDefault(DefaultExpression node) + { + _state = State.CreateEvaluatable(typeof(DefaultExpression), containsCapturedVariable: false); + return node; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override E VisitExtension(E extension) + { + if (extension is QueryRootExpression queryRoot) + { + var queryProvider = queryRoot.QueryProvider; + if (_currentQueryProvider == null) + { + _currentQueryProvider = queryProvider; + } + else if (!ReferenceEquals(queryProvider, _currentQueryProvider)) + { + throw new InvalidOperationException(CoreStrings.ErrorInvalidQueryable); + } + + // Visit after detaching query provider since custom query roots can have additional components + extension = queryRoot.DetachQueryProvider(); + + // The following is somewhat hacky. We're going to visit the query root's children via VisitChildren - this is primarily for + // FromSqlQueryRootExpression. Since the query root itself is never evaluatable, its children should all be handled as + // evaluatable roots - we set _evaluateRoot and do that in Visit. + // In addition, FromSqlQueryRootExpression's Arguments need to be a parameter rather than constant, so we set _inLambda to + // make that happen (quite hacky, but was done this way in the old ParameterExtractingEV as well). Think about a better way. + _evaluateRoot = true; + var parentInLambda = _inLambda; + _inLambda = false; + var visitedExtension = base.VisitExtension(extension); + _evaluateRoot = false; + _inLambda = parentInLambda; + _state = State.NoEvaluatability; + return visitedExtension; + } + + return base.VisitExtension(extension); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override E VisitInvocation(InvocationExpression invocation) + { + var expression = Visit(invocation.Expression, out var expressionState); + var state = expressionState.StateType; + var arguments = Visit(invocation.Arguments, ref state, out var argumentStates); + + switch (state) + { + case StateType.EvaluatableWithCapturedVariable or StateType.EvaluatableWithoutCapturedVariable + when IsGenerallyEvaluatable(invocation): + _state = State.CreateEvaluatable(typeof(InvocationExpression), state is StateType.EvaluatableWithCapturedVariable); + break; + + case StateType.NoEvaluatability: + _state = State.NoEvaluatability; + break; + + default: + List? children = null; + + if (expressionState.IsEvaluatable) + { + expression = ProcessEvaluatableRoot(expression, ref expressionState); + } + + if (expressionState.ContainsEvaluatable && _calculatingPath) + { + children = + [ + expressionState.Path! with { PathFromParent = static e => E.Property(e, nameof(InvocationExpression.Expression)) } + ]; + } + + arguments = EvaluateList( + ((IReadOnlyList?)arguments) ?? invocation.Arguments, + argumentStates, + children, + static i => e => + E.Call( + E.Property(e, nameof(InvocationExpression.Arguments)), + ReadOnlyCollectionIndexerGetter, + arguments: [E.Constant(i)])); + + _state = children is null + ? State.NoEvaluatability + : State.CreateContainsEvaluatable(typeof(InvocationExpression), children); + break; + } + + StateArrayPool.Return(argumentStates); + return invocation.Update(expression, ((IReadOnlyList?)arguments) ?? invocation.Arguments); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override E VisitIndex(IndexExpression index) + { + var @object = Visit(index.Object, out var objectState); + var state = objectState.StateType; + var arguments = Visit(index.Arguments, ref state, out var argumentStates); + + switch (state) + { + case StateType.EvaluatableWithCapturedVariable or StateType.EvaluatableWithoutCapturedVariable: + _state = State.CreateEvaluatable(typeof(IndexExpression), state is StateType.EvaluatableWithCapturedVariable); + break; + + case StateType.NoEvaluatability: + _state = State.NoEvaluatability; + break; + + default: + List? children = null; + + if (objectState.IsEvaluatable) + { + @object = ProcessEvaluatableRoot(@object, ref objectState); + } + + if (objectState.ContainsEvaluatable && _calculatingPath) + { + children = [objectState.Path! with { PathFromParent = static e => E.Property(e, nameof(IndexExpression.Object)) }]; + } + + arguments = EvaluateList( + ((IReadOnlyList?)arguments) ?? index.Arguments, + argumentStates, + children, + static i => e => + E.Call( + E.Property(e, nameof(IndexExpression.Arguments)), + ReadOnlyCollectionIndexerGetter, + arguments: [E.Constant(i)])); + + _state = children is null + ? State.NoEvaluatability + : State.CreateContainsEvaluatable(typeof(IndexExpression), children); + break; + } + + StateArrayPool.Return(argumentStates); + + // TODO: https://github.com/dotnet/runtime/issues/96626 + return index.Update(@object!, ((IReadOnlyList?)arguments) ?? index.Arguments); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override E VisitLambda(Expression lambda) + { + var oldInLambda = _inLambda; + _inLambda = true; + + var body = Visit(lambda.Body, out _state); + lambda = lambda.Update(body, lambda.Parameters); + + if (_state.StateType is StateType.EvaluatableWithCapturedVariable or StateType.EvaluatableWithoutCapturedVariable) + { + // The lambda body is evaluatable. If all lambda parameters are also in the _allowedParameters set (this happens for + // Select() over an evaluatable source, see VisitMethodCall()), then the whole lambda is evaluatable. Otherwise, evaluate + // the body. + if (lambda.Parameters.All(parameter => _evaluatableParameters.Contains(parameter))) + { + _state = State.CreateEvaluatable(typeof(LambdaExpression), _state.ContainsCapturedVariable); + return lambda; + } + + lambda = lambda.Update(ProcessEvaluatableRoot(lambda.Body, ref _state), lambda.Parameters); + } + + if (_state.ContainsEvaluatable) + { + _state = State.CreateContainsEvaluatable( + typeof(LambdaExpression), + [_state.Path! with { PathFromParent = static e => E.Property(e, nameof(Expression.Body)) }]); + } + + _inLambda = oldInLambda; + + return lambda; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override E VisitMember(MemberExpression member) + { + // Static member access - notably required for EF.Functions, but also for various translations (DateTime.Now). + if (member.Expression is null) + { + _state = IsGenerallyEvaluatable(member) + ? State.CreateEvaluatable(typeof(MemberExpression), containsCapturedVariable: false) + : State.NoEvaluatability; + return member; + } + + var expression = Visit(member.Expression, out _state); + + if (_state.IsEvaluatable) + { + // If the query contains a captured variable that's a nested IQueryable, inline it into the main query. + // Otherwise, evaluation of a terminating operator up the call chain will cause us to execute the query and do another + // roundtrip. + // Note that we only do this when the MemberExpression is typed as IQueryable/IOrderedQueryable; this notably excludes + // DbSet captured variables integrated directly into the query, as that also evaluates e.g. context.Order in + // context.Order.FromSqlInterpolated(), which fails. + if (member.Type.IsConstructedGenericType + && member.Type.GetGenericTypeDefinition() is var genericTypeDefinition + && (genericTypeDefinition == typeof(IQueryable<>) || genericTypeDefinition == typeof(IOrderedQueryable<>)) + && Evaluate(member) is IQueryable queryable) + { + return Visit(queryable.Expression); + } + + if (IsGenerallyEvaluatable(member)) + { + _state = State.CreateEvaluatable(typeof(MemberExpression), _state.ContainsCapturedVariable); + return member.Update(expression); + } + + expression = ProcessEvaluatableRoot(expression, ref _state); + } + + if (_state.ContainsEvaluatable && _calculatingPath) + { + _state = State.CreateContainsEvaluatable( + typeof(MemberExpression), + [_state.Path! with { PathFromParent = static e => E.Property(e, nameof(MemberExpression.Expression)) }]); + } + + return member.Update(expression); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override E VisitMethodCall(MethodCallExpression methodCall) + { + var method = methodCall.Method; + + // Handle some special, well-known functions + // If this is a call to EF.Constant(), or EF.Parameter(), then examine the operand; it it's isn't evaluatable (i.e. contains a + // reference to a database table), throw immediately. Otherwise, evaluate the operand (either as a constant or as a parameter) and + // return that. + if (method.DeclaringType == typeof(EF)) + { + switch (method.Name) + { + case nameof(EF.Constant): + { + if (_calculatingPath) + { + throw new InvalidOperationException("EF.Constant is not supported when using precompiled queries"); + } + + var argument = Visit(methodCall.Arguments[0], out var argumentState); + + if (!argumentState.IsEvaluatable) + { + throw new InvalidOperationException(CoreStrings.EFConstantWithNonEvaluatableArgument); + } + + argumentState = argumentState with + { + StateType = StateType.EvaluatableWithoutCapturedVariable, ForceConstantization = true + }; + var evaluatedArgument = ProcessEvaluatableRoot(argument, ref argumentState); + _state = argumentState; + return evaluatedArgument; + } + + case nameof(EF.Parameter): + { + var argument = Visit(methodCall.Arguments[0], out var argumentState); + + if (!argumentState.IsEvaluatable) + { + throw new InvalidOperationException(CoreStrings.EFParameterWithNonEvaluatableArgument); + } + + argumentState = argumentState with { StateType = StateType.EvaluatableWithCapturedVariable }; + var evaluatedArgument = ProcessEvaluatableRoot(argument, ref argumentState); + _state = argumentState; + return evaluatedArgument; + } + } + } + + // Regular/arbitrary method handling from here on + + // First, visit the object and all arguments, saving states as well + var @object = Visit(methodCall.Object, out var singleState); + var state = singleState.StateType; + var arguments = Visit(methodCall.Arguments, ref state, out var argumentStates); + + // The following identifies Select(), and its lambda parameters in a special list which allows us to evaluate them. + if (method.DeclaringType == typeof(Enumerable) + && method.Name == nameof(Enumerable.Select) + && argumentStates[0].IsEvaluatable + && methodCall.Arguments[1] is LambdaExpression lambda) + { + foreach (var parameter in lambda.Parameters) + { + _evaluatableParameters.Add(parameter); + } + + // Revisit with the updated _evaluatableParameters. + state = singleState.StateType; + arguments = Visit(methodCall.Arguments, ref state, out argumentStates); + } + + // We've visited everything and know all the states. + switch (state) + { + case StateType.EvaluatableWithCapturedVariable or StateType.EvaluatableWithoutCapturedVariable + when IsGenerallyEvaluatable(methodCall): + _state = State.CreateEvaluatable(typeof(MethodCallExpression), state is StateType.EvaluatableWithCapturedVariable); + break; + + case StateType.NoEvaluatability: + _state = State.NoEvaluatability; + break; + + default: + List? children = null; + + if (singleState.IsEvaluatable) + { + @object = ProcessEvaluatableRoot(@object, ref singleState); + } + + if (singleState.ContainsEvaluatable && _calculatingPath) + { + children = [singleState.Path! with { PathFromParent = static e => E.Property(e, nameof(MethodCallExpression.Object)) }]; + } + + // To support [NotParameterized] and indexer method arguments - which force evaluation as constant - go over the parameters + // and modify the states as needed + ParameterInfo[]? parameterInfos = null; + for (var i = 0; i < methodCall.Arguments.Count; i++) + { + var argumentState = argumentStates[i]; + + if (argumentState.IsEvaluatable) + { + parameterInfos ??= methodCall.Method.GetParameters(); + if (parameterInfos[i].GetCustomAttribute() is not null + || _model.IsIndexerMethod(methodCall.Method)) + { + argumentStates[i] = argumentState with + { + StateType = StateType.EvaluatableWithoutCapturedVariable, ForceConstantization = true + }; + } + } + } + + arguments = EvaluateList( + ((IReadOnlyList?)arguments) ?? methodCall.Arguments, + argumentStates, + children, + static i => e => + E.Call( + E.Property(e, nameof(MethodCallExpression.Arguments)), + ReadOnlyCollectionIndexerGetter, + arguments: [E.Constant(i)])); + + _state = children is null + ? State.NoEvaluatability + : State.CreateContainsEvaluatable(typeof(MethodCallExpression), children); + break; + } + + return methodCall.Update(@object, ((IReadOnlyList?)arguments) ?? methodCall.Arguments); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override E VisitNewArray(NewArrayExpression newArray) + { + StateType state = default; + var expressions = Visit(newArray.Expressions, ref state, out var expressionStates, poolExpressionStates: false); + + switch (state) + { + case StateType.EvaluatableWithCapturedVariable or StateType.EvaluatableWithoutCapturedVariable + or StateType.Unknown // Zero expressions + when IsGenerallyEvaluatable(newArray): + _state = State.CreateEvaluatable( + typeof(NewExpression), + state is StateType.EvaluatableWithCapturedVariable, + // See note below on EvaluateChildren + notEvaluatableAsRootHandler: () => EvaluateChildren(newArray, expressions, expressionStates)); + break; + + case StateType.NoEvaluatability: + _state = State.NoEvaluatability; + break; + + default: + return EvaluateChildren(newArray, expressions, expressionStates); + } + + return newArray.Update(((IReadOnlyList?)expressions) ?? newArray.Expressions); + + // We don't parameterize NewArrayExpression when its an evaluatable root, since we want to allow translating new[] { x, y } to + // e.g. IN (x, y) rather than parameterizing the whole thing. But bubble up the evaluatable state so it may get evaluated at a + // higher level. + // To support that, when the NewArrayExpression is evaluatable, we include a nonEvaluatableAsRootHandler lambda in the returned + // state, which gets invoked up the stack, calling this method. This evaluates the NewArrayExpression's children, but not the + // NewArrayExpression. + NewArrayExpression EvaluateChildren(NewArrayExpression newArray, E[]? expressions, State[] expressionStates) + { + List? children = null; + + expressions = EvaluateList( + ((IReadOnlyList?)expressions) ?? newArray.Expressions, + expressionStates, + children, + i => e => E.Call( + E.Property(e, nameof(NewArrayExpression.Expressions)), + ReadOnlyCollectionIndexerGetter, + arguments: [E.Constant(i)])); + + _state = children is null + ? State.NoEvaluatability + : State.CreateContainsEvaluatable(typeof(NewArrayExpression), children); + + return newArray.Update(((IReadOnlyList?)expressions) ?? newArray.Expressions); + } + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override E VisitNew(NewExpression @new) + { + StateType state = default; + var arguments = Visit(@new.Arguments, ref state, out var argumentStates, poolExpressionStates: false); + + switch (state) + { + case StateType.EvaluatableWithCapturedVariable or StateType.EvaluatableWithoutCapturedVariable + or StateType.Unknown // Zero expressions + when IsGenerallyEvaluatable(@new): + _state = State.CreateEvaluatable( + typeof(NewExpression), + state is StateType.EvaluatableWithCapturedVariable, + // See note below on EvaluateChildren + notEvaluatableAsRootHandler: () => EvaluateChildren(@new, arguments, argumentStates)); + break; + + case StateType.NoEvaluatability: + _state = State.NoEvaluatability; + break; + + default: + return EvaluateChildren(@new, arguments, argumentStates); + } + + return @new.Update(((IReadOnlyList?)arguments) ?? @new.Arguments); + + // Although we allow NewExpression to be evaluated within larger tree fragments, we don't constantize them when they're the + // evaluatable root, since that would embed arbitrary user type instances in our shaper. + // To support that, when the NewExpression is evaluatable, we include a nonEvaluatableAsRootHandler lambda in the returned state, + // which gets invoked up the stack, calling this method. This evaluates the NewExpression's children, but not the NewExpression. + NewExpression EvaluateChildren(NewExpression @new, E[]? arguments, State[] argumentStates) + { + List? children = null; + + arguments = EvaluateList( + ((IReadOnlyList?)arguments) ?? @new.Arguments, + argumentStates, + children, + i => e => E.Call( + E.Property(e, nameof(NewExpression.Arguments)), + ReadOnlyCollectionIndexerGetter, + arguments: [E.Constant(i)])); + + _state = children is null + ? State.NoEvaluatability + : State.CreateContainsEvaluatable(typeof(NewExpression), children); + + return @new.Update(((IReadOnlyList?)arguments) ?? @new.Arguments); + } + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override E VisitParameter(ParameterExpression parameterExpression) + { + // ParameterExpressions are lambda parameters, which we cannot evaluate. + // However, _allowedParameters is a mechanism to allow evaluating Select(), see VisitMethodCall. + _state = _evaluatableParameters.Contains(parameterExpression) + ? State.CreateEvaluatable(typeof(ParameterExpression), containsCapturedVariable: false) + : State.NoEvaluatability; + + return parameterExpression; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override E VisitTypeBinary(TypeBinaryExpression typeBinary) + { + var expression = Visit(typeBinary.Expression, out _state); + + if (_state.IsEvaluatable) + { + if (IsGenerallyEvaluatable(typeBinary)) + { + _state = State.CreateEvaluatable(typeof(TypeBinaryExpression), _state.ContainsCapturedVariable); + return typeBinary.Update(expression); + } + + expression = ProcessEvaluatableRoot(expression, ref _state); + } + + if (_state.ContainsEvaluatable && _calculatingPath) + { + _state = State.CreateContainsEvaluatable( + typeof(TypeBinaryExpression), + [_state.Path! with { PathFromParent = static e => E.Property(e, nameof(TypeBinaryExpression.Expression)) }]); + } + + return typeBinary.Update(expression); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override E VisitMemberInit(MemberInitExpression memberInit) + { + var @new = (NewExpression)Visit(memberInit.NewExpression, out var newState); + var state = newState.StateType; + var bindings = Visit(memberInit.Bindings, VisitMemberBinding, ref state, out var bindingStates, poolExpressionStates: false); + + switch (state) + { + case StateType.EvaluatableWithCapturedVariable or StateType.EvaluatableWithoutCapturedVariable + when IsGenerallyEvaluatable(memberInit): + _state = State.CreateEvaluatable( + typeof(InvocationExpression), + state is StateType.EvaluatableWithCapturedVariable, + notEvaluatableAsRootHandler: () => EvaluateChildren(memberInit, @new, newState, bindings, bindingStates)); + break; + + case StateType.NoEvaluatability: + _state = State.NoEvaluatability; + break; + + default: + return EvaluateChildren(memberInit, @new, newState, bindings, bindingStates); + } + + return memberInit.Update(@new, ((IReadOnlyList?)bindings) ?? memberInit.Bindings); + + // Although we allow MemberInitExpression to be evaluated within larger tree fragments, we don't constantize them when they're the + // evaluatable root, since that would embed arbitrary user type instances in our shaper. + // To support that, when the MemberInitExpression is evaluatable, we include a nonEvaluatableAsRootHandler lambda in the returned + // state, which gets invoked up the stack, calling this method. This evaluates the MemberInitExpression's children, but not the + // MemberInitExpression. + MemberInitExpression EvaluateChildren( + MemberInitExpression memberInit, + NewExpression @new, + State newState, + MemberBinding[]? bindings, + State[] bindingStates) + { + // If the NewExpression is evaluatable but one of the bindings isn't, we can't evaluate only the NewExpression + // (MemberInitExpression requires a NewExpression and doesn't accept ParameterException). However, we may still need to + // evaluate constructor arguments in the NewExpression. + if (newState.IsEvaluatable) + { + @new = (NewExpression)newState.NotEvaluatableAsRootHandler!(); + } + + List? children = null; + + if (newState.ContainsEvaluatable && _calculatingPath) + { + children = + [ + newState.Path! with { PathFromParent = static e => E.Property(e, nameof(MemberInitExpression.NewExpression)) } + ]; + } + + for (var i = 0; i < memberInit.Bindings.Count; i++) + { + var bindingState = bindingStates[i]; + + if (bindingState.IsEvaluatable) + { + bindings ??= memberInit.Bindings.ToArray(); + var binding = (MemberAssignment)bindings[i]; + bindings[i] = binding.Update(ProcessEvaluatableRoot(binding.Expression, ref bindingState)); + bindingStates[i] = bindingState; + } + + if (bindingState.ContainsEvaluatable && _calculatingPath) + { + children ??= []; + var index = i; // i gets mutated so make a copy for capturing below + children.Add( + bindingState.Path! with + { + PathFromParent = e => + E.Property( + E.Convert( + E.Call( + E.Property(e, nameof(MemberInitExpression.Bindings)), + ReadOnlyMemberBindingCollectionIndexerGetter, + arguments: [E.Constant(index)]), typeof(MemberAssignment)), + MemberAssignmentExpressionProperty) + }); + } + } + + _state = children is null + ? State.NoEvaluatability + : State.CreateContainsEvaluatable(typeof(MemberInitExpression), children); + + return memberInit.Update(@new, ((IReadOnlyList?)bindings) ?? memberInit.Bindings); + } + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override E VisitListInit(ListInitExpression listInit) + { + // First, visit the NewExpression and all initializers, saving states as well + var @new = (NewExpression)Visit(listInit.NewExpression, out var newState); + var state = newState.StateType; + var initializers = listInit.Initializers; + var initializerArgumentStates = new State[listInit.Initializers.Count][]; + + IReadOnlyList[]? visitedInitializersArguments = null; + + for (var i = 0; i < initializers.Count; i++) + { + var initializer = initializers[i]; + + var visitedArguments = Visit(initializer.Arguments, ref state, out var argumentStates); + if (visitedArguments is not null) + { + if (visitedInitializersArguments is null) + { + visitedInitializersArguments = new IReadOnlyList[initializers.Count]; + for (var j = 0; j < i; j++) + { + visitedInitializersArguments[j] = initializers[j].Arguments; + } + } + } + + if (visitedInitializersArguments is not null) + { + visitedInitializersArguments[i] = (IReadOnlyList?)visitedArguments ?? initializer.Arguments; + } + + initializerArgumentStates[i] = argumentStates; + } + + // We've visited everything and have both our aggregate state, and the states of all initializer expressions. + switch (state) + { + case StateType.EvaluatableWithCapturedVariable or StateType.EvaluatableWithoutCapturedVariable + when IsGenerallyEvaluatable(listInit): + _state = State.CreateEvaluatable(typeof(ListInitExpression), state is StateType.EvaluatableWithCapturedVariable); + break; + + case StateType.NoEvaluatability: + _state = State.NoEvaluatability; + break; + + default: + // If the NewExpression is evaluatable but one of the bindings isn't, we can't evaluate only the NewExpression + // (ListInitExpression requires a NewExpression and doesn't accept ParameterException). However, we may still need to + // evaluate constructor arguments in the NewExpression. + if (newState.IsEvaluatable) + { + @new = (NewExpression)newState.NotEvaluatableAsRootHandler!(); + } + + List? children = null; + + if (newState.ContainsEvaluatable) + { + children = + [ + newState.Path! with { PathFromParent = static e => E.Property(e, nameof(MethodCallExpression.Object)) } + ]; + } + + for (var i = 0; i < initializers.Count; i++) + { + var initializer = initializers[i]; + + var visitedArguments = EvaluateList( + visitedInitializersArguments is null + ? initializer.Arguments + : visitedInitializersArguments[i], + initializerArgumentStates[i], + children, + static i => e => + E.Call( + E.Property(e, nameof(MethodCallExpression.Arguments)), + ReadOnlyCollectionIndexerGetter, + arguments: [E.Constant(i)])); + + if (visitedArguments is not null && visitedInitializersArguments is null) + { + visitedInitializersArguments = new IReadOnlyList[initializers.Count]; + for (var j = 0; j < i; j++) + { + visitedInitializersArguments[j] = initializers[j].Arguments; + } + } + + if (visitedInitializersArguments is not null) + { + visitedInitializersArguments[i] = (IReadOnlyList?)visitedArguments ?? initializer.Arguments; + } + } + + _state = children is null + ? State.NoEvaluatability + : State.CreateContainsEvaluatable(typeof(ListInitExpression), children); + break; + } + + foreach (var argumentState in initializerArgumentStates) + { + StateArrayPool.Return(argumentState); + } + + if (visitedInitializersArguments is null) + { + return listInit.Update(@new, listInit.Initializers); + } + + var visitedInitializers = new ElementInit[initializers.Count]; + for (var i = 0; i < visitedInitializersArguments.Length; i++) + { + visitedInitializers[i] = initializers[i].Update(visitedInitializersArguments[i]); + } + + return listInit.Update(@new, visitedInitializers); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override E VisitUnary(UnaryExpression unary) + { + var operand = Visit(unary.Operand, out var operandState); + + switch (operandState.StateType) + { + case StateType.EvaluatableWithCapturedVariable or StateType.EvaluatableWithoutCapturedVariable + or StateType.Unknown // Null operand + when IsGenerallyEvaluatable(unary): + _state = State.CreateEvaluatable( + typeof(UnaryExpression), + _state.ContainsCapturedVariable, + // See note below on EvaluateChildren + notEvaluatableAsRootHandler: () => EvaluateOperand(unary, operand, operandState)); + break; + + case StateType.NoEvaluatability: + _state = State.NoEvaluatability; + break; + + default: + return EvaluateOperand(unary, operand, operandState); + } + + return unary.Update(operand); + + // There are some cases of Convert nodes which we shouldn't evaluate when they're at the top of an evaluatable root (but can + // evaluate when they're part of a larger fragment). + // To support that, when the UnaryExpression is evaluatable, we include a nonEvaluatableAsRootHandler lambda in the returned state, + // which gets invoked up the stack, calling this method. This evaluates the UnaryExpression's operand, but not the UnaryExpression. + UnaryExpression EvaluateOperand(UnaryExpression unary, Expression operand, State operandState) + { + if (operandState.IsEvaluatable) + { + operand = ProcessEvaluatableRoot(operand, ref operandState); + } + + if (_state.ContainsEvaluatable) + { + _state = _calculatingPath + ? State.CreateContainsEvaluatable( + typeof(UnaryExpression), + [_state.Path! with { PathFromParent = static e => E.Property(e, nameof(UnaryExpression.Operand)) }]) + : State.NoEvaluatability; + } + + return unary.Update(operand); + } + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override ElementInit VisitElementInit(ElementInit node) + => throw new UnreachableException(); // Handled in VisitListInit + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override MemberListBinding VisitMemberListBinding(MemberListBinding node) + => throw new InvalidOperationException(CoreStrings.MemberListBindingNotSupported); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override MemberMemberBinding VisitMemberMemberBinding(MemberMemberBinding node) + => throw new InvalidOperationException(CoreStrings.MemberMemberBindingNotSupported); + + #endregion Visitation implementations + + #region Unsupported node types + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override E VisitBlock(BlockExpression node) + => throw new NotSupportedException(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override CatchBlock VisitCatchBlock(CatchBlock node) + => throw new NotSupportedException(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override E VisitDebugInfo(DebugInfoExpression node) + => throw new NotSupportedException(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override E VisitDynamic(DynamicExpression node) + => throw new NotSupportedException(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override E VisitGoto(GotoExpression node) + => throw new NotSupportedException(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override LabelTarget VisitLabelTarget(LabelTarget? node) + => throw new NotSupportedException(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override E VisitLabel(LabelExpression node) + => throw new NotSupportedException(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override E VisitLoop(LoopExpression node) + => throw new NotSupportedException(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override E VisitRuntimeVariables(RuntimeVariablesExpression node) + => throw new NotSupportedException(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override E VisitSwitch(SwitchExpression node) + => throw new NotSupportedException(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override SwitchCase VisitSwitchCase(SwitchCase node) + => throw new NotSupportedException(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override E VisitTry(TryExpression node) + => throw new NotSupportedException(); + + #endregion Unsupported node types + + private static StateType CombineStateTypes(StateType stateType1, StateType stateType2) + => (stateType1, stateType2) switch + { + (StateType.Unknown, var s) => s, + (var s, StateType.Unknown) => s, + + (StateType.NoEvaluatability, StateType.NoEvaluatability) => StateType.NoEvaluatability, + + (StateType.EvaluatableWithoutCapturedVariable, StateType.EvaluatableWithoutCapturedVariable) + => StateType.EvaluatableWithoutCapturedVariable, + + (StateType.EvaluatableWithCapturedVariable, + StateType.EvaluatableWithCapturedVariable or StateType.EvaluatableWithoutCapturedVariable) + or + (StateType.EvaluatableWithCapturedVariable or StateType.EvaluatableWithoutCapturedVariable, + StateType.EvaluatableWithCapturedVariable) + => StateType.EvaluatableWithCapturedVariable, + + _ => StateType.ContainsEvaluatable + }; + + private E[]? Visit( + ReadOnlyCollection expressions, + ref StateType aggregateStateType, + out State[] expressionStates, + bool poolExpressionStates = true) + => Visit(expressions, Visit, ref aggregateStateType, out expressionStates, poolExpressionStates); + + // This follows the ExpressionVisitor.Visit(ReadOnlyCollection) pattern. + private T[]? Visit( + ReadOnlyCollection expressions, + Func elementVisitor, + ref StateType aggregateStateType, + out State[] expressionStates, + bool poolExpressionStates = true) + { + if (expressions.Count == 0) + { + aggregateStateType = CombineStateTypes(aggregateStateType, StateType.EvaluatableWithoutCapturedVariable); + expressionStates = []; + return null; + } + + expressionStates = poolExpressionStates ? StateArrayPool.Rent(expressions.Count) : new State[expressions.Count]; + + T[]? newExpressions = null; + for (var i = 0; i < expressions.Count; i++) + { + var oldExpression = expressions[i]; + var newExpression = elementVisitor(oldExpression); + var expressionState = _state; + + if (!ReferenceEquals(newExpression, oldExpression) && newExpressions is null) + { + newExpressions = new T[expressions.Count]; + for (var j = 0; j < i; j++) + { + newExpressions[j] = expressions[j]; + } + } + + if (newExpressions is not null) + { + newExpressions[i] = newExpression; + } + + expressionStates[i] = expressionState; + + aggregateStateType = CombineStateTypes(aggregateStateType, expressionState.StateType); + } + + return newExpressions; + } + + private E[]? EvaluateList( + IReadOnlyList expressions, + State[] expressionStates, + List? children, + Func> pathFromParentGenerator) + { + // This allows us to make in-place changes in the expression array when the previous visitation pass made modifications (and so + // returned a mutable array). This removes an additional copy that would be needed. + var visitedExpressions = expressions as E[]; + + for (var i = 0; i < expressions.Count; i++) + { + var argumentState = expressionStates[i]; + if (argumentState.IsEvaluatable) + { + if (visitedExpressions is null) + { + visitedExpressions = new E[expressions.Count]; + for (var j = 0; j < i; j++) + { + visitedExpressions[j] = expressions[j]; + } + } + + visitedExpressions[i] = ProcessEvaluatableRoot(expressions[i], ref argumentState); + expressionStates[i] = argumentState; + } + else if (visitedExpressions is not null) + { + visitedExpressions[i] = expressions[i]; + } + + if (argumentState.ContainsEvaluatable && _calculatingPath) + { + children ??= []; + children.Add(argumentState.Path! with { PathFromParent = pathFromParentGenerator(i) }); + } + } + + return visitedExpressions; + } + + [return: NotNullIfNotNull(nameof(evaluatableRoot))] + private E? ProcessEvaluatableRoot(E? evaluatableRoot, ref State state) + { + if (evaluatableRoot is null) + { + return null; + } + + var evaluateAsParameter = + // In some cases, constantization is forced by the context ([NotParameterized], EF.Constant) + !state.ForceConstantization + && _parameterize + && ( + // If the nodes contains a captured variable somewhere within it, we evaluate as a parameter. + state.ContainsCapturedVariable + // We don't evaluate as constant if we're not inside a lambda, i.e. in a top-level operator. This is to make sure that + // non-lambda arguments to e.g. Skip/Take are parameterized rather than evaluated as constant, since that would produce + // different SQLs for each value. + || !_inLambda + || (evaluatableRoot is MemberExpression member + && (member.Expression is not null || member.Member is not FieldInfo { IsInitOnly: true }))); + + // We have some cases where a node is evaluatable, but only as part of a larger subtree, and should not be evaluated as a tree root. + // For these cases, the node's state has a notEvaluatableAsRootHandler lambda, which we can invoke to make evaluate the node's + // children (as needed), but not itself. + if (TryHandleNonEvaluatableAsRoot(evaluatableRoot, state, evaluateAsParameter, out var result)) + { + return result; + } + + var value = Evaluate(evaluatableRoot, out var parameterName, out var isContextAccessor); + + switch (value) + { + // If the query contains a nested IQueryable, e.g. Where(b => context.Blogs.Count()...), the context.Blogs parts gets + // evaluated as a parameter; visit its expression tree instead. + case IQueryable { Expression: var innerExpression }: + return Visit(innerExpression); + + case Expression innerExpression when !isContextAccessor: + return Visit(innerExpression); + } + + if (isContextAccessor) + { + // Context accessors (query filters accessing the context) never get constantized + evaluateAsParameter = true; + } + + if (evaluateAsParameter) + { + if (_parameterizedValues.TryGetValue(evaluatableRoot, out var cachedParameter)) + { + // We're here when the same captured variable (or other fragment) is referenced more than once in the query; we want to + // use the same query parameter rather than sending it twice. + // Note that in path calculation (precompiled query), we don't have to do anything, as the path only needs to be returned + // once. + state = State.NoEvaluatability; + return cachedParameter; + } + + if (_calculatingPath) + { + state = new() + { + StateType = StateType.ContainsEvaluatable, + Path = new() + { + ExpressionType = state.ExpressionType!, + ParameterName = parameterName, + Children = Array.Empty() + } + }; + + // We still maintain _parameterValues since later parameter names are generated based on already-populated names. + _parameterValues.AddParameter(parameterName, null); + + return evaluatableRoot; + } + + // Regular parameter extraction mode; client-evaluate the subtree and replace it with a query parameter. + state = State.NoEvaluatability; + + _parameterValues.AddParameter(parameterName, value); + + return _parameterizedValues[evaluatableRoot] = E.Parameter(evaluatableRoot.Type, parameterName); + } + + // Evaluate as constant + state = State.NoEvaluatability; + + // In precompilation mode, we don't care about constant evaluation since the expression tree itself isn't going to get used. + // We only care about generating code for extracting captured variables, so ignore. + if (_calculatingPath) + { + // TODO: EF.Constant is probably incompatible with precompilation, may need to throw (but not here, only from EF.Constant) + return evaluatableRoot; + } + + var returnType = evaluatableRoot.Type; + var constantExpression = E.Constant(value, value?.GetType() ?? returnType); + + return constantExpression.Type != returnType + ? E.Convert(constantExpression, returnType) + : constantExpression; + + bool TryHandleNonEvaluatableAsRoot(E root, State state, bool asParameter, [NotNullWhen(true)] out Expression? result) + { + switch (root) + { + // We don't parameterize NewArrayExpression when its an evaluatable root, since we want to allow translating new[] { x, y } + // to e.g. IN (x, y) rather than parameterizing the whole thing. But bubble up the evaluatable state so it may get evaluated + // at a higher level. + case NewArrayExpression when asParameter: + // We don't constantize NewExpression/MemberInitExpression since that would embed arbitrary user type instances in our + // shaper. + case NewExpression or MemberInitExpression when !asParameter: + // There are some cases of Convert nodes which we shouldn't evaluate when they're at the top of an evaluatable root (but can + // evaluate when they're part of a larger fragment). + case UnaryExpression unary when PreserveConvertNode(unary): + result = state.NotEvaluatableAsRootHandler!(); + return true; + + default: + result = null; + return false; + } + + bool PreserveConvertNode(E expression) + { + if (expression is UnaryExpression { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked } unaryExpression) + { + if (unaryExpression.Type == typeof(object) + || unaryExpression.Type == typeof(Enum) + || unaryExpression.Operand.Type.UnwrapNullableType().IsEnum) + { + return true; + } + + var innerType = unaryExpression.Operand.Type.UnwrapNullableType(); + if (unaryExpression.Type.UnwrapNullableType() == typeof(int) + && (innerType == typeof(byte) + || innerType == typeof(sbyte) + || innerType == typeof(char) + || innerType == typeof(short) + || innerType == typeof(ushort))) + { + return true; + } + + return PreserveConvertNode(unaryExpression.Operand); + } + + return false; + } + } + } + + private object? Evaluate(E? expression) + => Evaluate(expression, out _, out _); + + private object? Evaluate(E? expression, out string parameterName, out bool isContextAccessor) + { + var value = EvaluateCore(expression, out var tempParameterName, out isContextAccessor); + parameterName = tempParameterName ?? "p"; + + var compilerPrefixIndex = parameterName.LastIndexOf('>'); + if (compilerPrefixIndex != -1) + { + parameterName = parameterName[(compilerPrefixIndex + 1)..]; + } + + parameterName = $"{QueryCompilationContext.QueryParameterPrefix}{parameterName}_{_parameterValues.ParameterValues.Count}"; + + return value; + + object? EvaluateCore(E? expression, out string? parameterName, out bool isContextAccessor) + { + parameterName = null; + isContextAccessor = false; + + if (expression == null) + { + return null; + } + + if (_generateContextAccessors) + { + var visited = _contextParameterReplacer.Visit(expression); + + if (visited != expression) + { + parameterName = QueryFilterPrefix + + (RemoveConvert(expression) is MemberExpression { Member.Name: var memberName } ? ("__" + memberName) : "__p"); + isContextAccessor = true; + + return E.Lambda(visited, _contextParameterReplacer.ContextParameterExpression); + } + + static E RemoveConvert(E expression) + => expression is UnaryExpression { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked } unaryExpression + ? RemoveConvert(unaryExpression.Operand) + : expression; + } + + switch (expression) + { + case MemberExpression memberExpression: + var instanceValue = EvaluateCore(memberExpression.Expression, out parameterName, out isContextAccessor); + try + { + switch (memberExpression.Member) + { + case FieldInfo fieldInfo: + parameterName = parameterName is null ? fieldInfo.Name : $"{parameterName}_{fieldInfo.Name}"; + return fieldInfo.GetValue(instanceValue); + + case PropertyInfo propertyInfo: + parameterName = parameterName is null ? propertyInfo.Name : $"{parameterName}_{propertyInfo.Name}"; + return propertyInfo.GetValue(instanceValue); + } + } + catch + { + // Try again when we compile the delegate + } + + break; + + case ConstantExpression constantExpression: + return constantExpression.Value; + + case MethodCallExpression methodCallExpression: + parameterName = methodCallExpression.Method.Name; + break; + + case UnaryExpression { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked } unaryExpression + when (unaryExpression.Type.UnwrapNullableType() == unaryExpression.Operand.Type): + return EvaluateCore(unaryExpression.Operand, out parameterName, out isContextAccessor); + } + + try + { + return E.Lambda>( + E.Convert(expression, typeof(object))) + .Compile(preferInterpretation: true) + .Invoke(); + } + catch (Exception exception) + { + throw new InvalidOperationException( + _logger.ShouldLogSensitiveData() + ? CoreStrings.ExpressionParameterizationExceptionSensitive(expression) + : CoreStrings.ExpressionParameterizationException, + exception); + } + } + } + + private bool IsGenerallyEvaluatable(E expression) + => _evaluatableExpressionFilter.IsEvaluatableExpression(expression, _model) + && (_parameterize + // Don't evaluate QueryableMethods if in compiled query + || !(expression is MethodCallExpression { Method: var method } && method.DeclaringType == typeof(Queryable))); + + private enum StateType + { + /// + /// A temporary initial state, before any children have been examined. + /// + Unknown, + + /// + /// Means that the current node is neither evaluatable, nor does it contains an evaluatable node. + /// + NoEvaluatability, + + /// + /// Whether the current node is evaluatable, i.e. contains no references to server-side resources, and does not contain any + /// captured variables. Such nodes can be evaluated and the result integrated as constants in the tree. + /// + EvaluatableWithoutCapturedVariable, + + /// + /// Whether the current node is evaluatable, i.e. contains no references to server-side resources, but contains captured + /// variables. Such nodes can be parameterized. + /// + EvaluatableWithCapturedVariable, + + /// + /// Whether the current node contains (parameterizable) evaluatable nodes anywhere within its children. + /// + ContainsEvaluatable + } + + private readonly record struct State + { + public static State CreateEvaluatable( + Type expressionType, + bool containsCapturedVariable, + Func? notEvaluatableAsRootHandler = null) + => new() + { + StateType = containsCapturedVariable + ? StateType.EvaluatableWithCapturedVariable + : StateType.EvaluatableWithoutCapturedVariable, + ExpressionType = expressionType, + NotEvaluatableAsRootHandler = notEvaluatableAsRootHandler + }; + + public static State CreateContainsEvaluatable(Type expressionType, IReadOnlyList children) + => new() + { + StateType = StateType.ContainsEvaluatable, + Path = new() { ExpressionType = expressionType, Children = children } + }; + + /// + /// Means that we're neither within an evaluatable subtree, nor on a node which contains one (and therefore needs to track the + /// path to it). + /// + public static readonly State NoEvaluatability = new() { StateType = StateType.NoEvaluatability }; + + public StateType StateType { get; init; } + + public Type? ExpressionType { get; init; } + + /// + /// A tree containing information on reaching all evaluatable nodes contained within this node. + /// + public PathNode? Path { get; init; } + + public bool ForceConstantization { get; init; } + + public Func? NotEvaluatableAsRootHandler { get; init; } + + public bool IsEvaluatable + => StateType is StateType.EvaluatableWithoutCapturedVariable or StateType.EvaluatableWithCapturedVariable; + + public bool ContainsCapturedVariable + => StateType is StateType.EvaluatableWithCapturedVariable; + + public bool ContainsEvaluatable + => StateType is StateType.ContainsEvaluatable; + + public override string ToString() + => StateType switch + { + StateType.NoEvaluatability => "No evaluatability", + StateType.EvaluatableWithoutCapturedVariable => "Evaluatable, no captured vars", + StateType.EvaluatableWithCapturedVariable => "Evaluatable, captured vars", + StateType.ContainsEvaluatable => "Contains evaluatable", + + _ => throw new UnreachableException() + }; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [EntityFrameworkInternal] + public record PathNode + { + /// + /// The type of the expression represented by this . + /// + public required Type ExpressionType { get; init; } + + /// + /// Children of this node which contain parameterizable fragments. + /// + public required IReadOnlyList? Children { get; init; } + + /// + /// A function that accepts the parent node, and returns an expression representing the path to this node from that parent + /// node. The returned expression can then be used to generate C# code that traverses the expression tree. + /// + public Func? PathFromParent { get; init; } + + /// + /// For nodes representing parameterizable roots, contains the preferred parameter name, generated based on the expression + /// node type/contents. + /// + public string? ParameterName { get; init; } + } + + private sealed class ContextParameterReplacer : ExpressionVisitor + { + private readonly Type _contextType; + + public ContextParameterReplacer(Type contextType) + { + ContextParameterExpression = Expression.Parameter(contextType, "context"); + _contextType = contextType; + } + + public ParameterExpression ContextParameterExpression { get; } + + [return: NotNullIfNotNull("expression")] + public override Expression? Visit(Expression? expression) + => expression?.Type != typeof(object) + && expression?.Type.IsAssignableFrom(_contextType) == true + ? ContextParameterExpression + : base.Visit(expression); + } + + private class DummyParameterValues : IParameterValues + { + private readonly Dictionary _parameterValues = new(); + + public IReadOnlyDictionary ParameterValues + => _parameterValues; + + public void AddParameter(string name, object? value) + => _parameterValues.Add(name, value); + } +} diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs index a78e3682f79..07b5982255b 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs @@ -49,7 +49,7 @@ private static readonly PropertyInfo QueryContextContextPropertyInfo private readonly EntityReferenceOptionalMarkingExpressionVisitor _entityReferenceOptionalMarkingExpressionVisitor; private readonly RemoveRedundantNavigationComparisonExpressionVisitor _removeRedundantNavigationComparisonExpressionVisitor; private readonly HashSet _parameterNames = []; - private readonly ParameterExtractingExpressionVisitor _parameterExtractingExpressionVisitor; + private readonly ExpressionTreeFuncletizer _funcletizer; private readonly INavigationExpansionExtensibilityHelper _extensibilityHelper; private readonly HashSet _nonCyclicAutoIncludeEntityTypes; @@ -80,14 +80,12 @@ public NavigationExpandingExpressionVisitor( _entityReferenceOptionalMarkingExpressionVisitor = new EntityReferenceOptionalMarkingExpressionVisitor(); _removeRedundantNavigationComparisonExpressionVisitor = new RemoveRedundantNavigationComparisonExpressionVisitor( queryCompilationContext.Logger); - _parameterExtractingExpressionVisitor = new ParameterExtractingExpressionVisitor( + _funcletizer = new ExpressionTreeFuncletizer( + _queryCompilationContext.Model, evaluatableExpressionFilter, - _parameters, _queryCompilationContext.ContextType, - _queryCompilationContext.Model, - _queryCompilationContext.Logger, - parameterize: false, - generateContextAccessors: true); + generateContextAccessors: true, + _queryCompilationContext.Logger); _nonCyclicAutoIncludeEntityTypes = !_queryCompilationContext.IgnoreAutoIncludes ? [] : null!; } @@ -210,8 +208,8 @@ protected override Expression VisitExtension(Expression extensionExpression) // Apply defining query only when it is not custom query root && entityQueryRootExpression.GetType() == typeof(EntityQueryRootExpression)) { - var processedDefiningQueryBody = - _parameterExtractingExpressionVisitor.ExtractParameters(definingQuery.Body, clearEvaluatedValues: false); + var processedDefiningQueryBody = _funcletizer.ExtractParameters( + definingQuery.Body, _parameters, parameterize: false, clearParameterizedValues: false); processedDefiningQueryBody = _queryTranslationPreprocessor.NormalizeQueryableMethod(processedDefiningQueryBody); processedDefiningQueryBody = _nullCheckRemovingExpressionVisitor.Visit(processedDefiningQueryBody); processedDefiningQueryBody = @@ -1754,8 +1752,8 @@ private Expression ApplyQueryFilter(IEntityType entityType, NavigationExpansionE if (!_parameterizedQueryFilterPredicateCache.TryGetValue(rootEntityType, out var filterPredicate)) { filterPredicate = queryFilter; - filterPredicate = (LambdaExpression)_parameterExtractingExpressionVisitor.ExtractParameters( - filterPredicate, clearEvaluatedValues: false); + filterPredicate = (LambdaExpression)_funcletizer.ExtractParameters( + filterPredicate, _parameters, parameterize: false, clearParameterizedValues: false); filterPredicate = (LambdaExpression)_queryTranslationPreprocessor.NormalizeQueryableMethod(filterPredicate); // We need to do entity equality, but that requires a full method call on a query root to properly flow the diff --git a/src/EFCore/Query/Internal/ParameterExtractingExpressionVisitor.cs b/src/EFCore/Query/Internal/ParameterExtractingExpressionVisitor.cs deleted file mode 100644 index d3057838ead..00000000000 --- a/src/EFCore/Query/Internal/ParameterExtractingExpressionVisitor.cs +++ /dev/null @@ -1,728 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; - -namespace Microsoft.EntityFrameworkCore.Query.Internal; - -/// -/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to -/// the same compatibility standards as public APIs. It may be changed or removed without notice in -/// any release. You should only use it directly in your code with extreme caution and knowing that -/// doing so can result in application failures when updating to a new Entity Framework Core release. -/// -public class ParameterExtractingExpressionVisitor : ExpressionVisitor -{ - private const string QueryFilterPrefix = "ef_filter"; - - private readonly IParameterValues _parameterValues; - private readonly IDiagnosticsLogger _logger; - private readonly bool _parameterize; - private readonly bool _generateContextAccessors; - private readonly EvaluatableExpressionFindingExpressionVisitor _evaluatableExpressionFindingExpressionVisitor; - private readonly ContextParameterReplacingExpressionVisitor _contextParameterReplacingExpressionVisitor; - - private readonly Dictionary _evaluatedValues = new(ExpressionEqualityComparer.Instance); - - private IDictionary _evaluatableExpressions; - private IQueryProvider? _currentQueryProvider; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public ParameterExtractingExpressionVisitor( - IEvaluatableExpressionFilter evaluatableExpressionFilter, - IParameterValues parameterValues, - Type contextType, - IModel model, - IDiagnosticsLogger logger, - bool parameterize, - bool generateContextAccessors) - { - _evaluatableExpressionFindingExpressionVisitor - = new EvaluatableExpressionFindingExpressionVisitor(evaluatableExpressionFilter, model, parameterize); - _parameterValues = parameterValues; - _logger = logger; - _parameterize = parameterize; - _generateContextAccessors = generateContextAccessors; - // The entry method will take care of populating this field always. So accesses should be safe. - _evaluatableExpressions = null!; - _contextParameterReplacingExpressionVisitor = _generateContextAccessors - ? new ContextParameterReplacingExpressionVisitor(contextType) - : null!; - } - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public virtual Expression ExtractParameters(Expression expression) - => ExtractParameters(expression, clearEvaluatedValues: true); - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public virtual Expression ExtractParameters(Expression expression, bool clearEvaluatedValues) - { - var oldEvaluatableExpressions = _evaluatableExpressions; - _evaluatableExpressions = _evaluatableExpressionFindingExpressionVisitor.Find(expression); - - try - { - return Visit(expression); - } - finally - { - _evaluatableExpressions = oldEvaluatableExpressions; - if (clearEvaluatedValues) - { - _evaluatedValues.Clear(); - } - } - } - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - [return: NotNullIfNotNull("expression")] - public override Expression? Visit(Expression? expression) - { - if (expression == null) - { - return null; - } - - if (_evaluatableExpressions.TryGetValue(expression, out var generateParameter) - && !PreserveInitializationConstant(expression, generateParameter) - && !PreserveConvertNode(expression)) - { - return Evaluate(expression, _parameterize && generateParameter); - } - - return base.Visit(expression); - } - - private static bool PreserveInitializationConstant(Expression expression, bool generateParameter) - => !generateParameter && expression is NewExpression or MemberInitExpression; - - private bool PreserveConvertNode(Expression expression) - { - if (expression is UnaryExpression unaryExpression - && (unaryExpression.NodeType == ExpressionType.Convert - || unaryExpression.NodeType == ExpressionType.ConvertChecked)) - { - if (unaryExpression.Type == typeof(object) - || unaryExpression.Type == typeof(Enum) - || unaryExpression.Operand.Type.UnwrapNullableType().IsEnum) - { - return true; - } - - var innerType = unaryExpression.Operand.Type.UnwrapNullableType(); - if (unaryExpression.Type.UnwrapNullableType() == typeof(int) - && (innerType == typeof(byte) - || innerType == typeof(sbyte) - || innerType == typeof(char) - || innerType == typeof(short) - || innerType == typeof(ushort))) - { - return true; - } - - return PreserveConvertNode(unaryExpression.Operand); - } - - return false; - } - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - protected override Expression VisitConditional(ConditionalExpression conditionalExpression) - { - var newTestExpression = TryGetConstantValue(conditionalExpression.Test) ?? Visit(conditionalExpression.Test); - - if (newTestExpression is ConstantExpression { Value: bool constantTestValue }) - { - return constantTestValue - ? Visit(conditionalExpression.IfTrue) - : Visit(conditionalExpression.IfFalse); - } - - return conditionalExpression.Update( - newTestExpression, - Visit(conditionalExpression.IfTrue), - Visit(conditionalExpression.IfFalse)); - } - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression) - { - // If this is a call to EF.Constant(), or EF.Parameter(), then examine the operand; it it's isn't evaluatable (i.e. contains a - // reference to a database table), throw immediately. Otherwise, evaluate the operand (either as a constant or as a parameter) and - // return that. - if (methodCallExpression.Method.DeclaringType == typeof(EF)) - { - switch (methodCallExpression.Method.Name) - { - case nameof(EF.Constant): - { - var operand = methodCallExpression.Arguments[0]; - if (!_evaluatableExpressions.TryGetValue(operand, out _)) - { - throw new InvalidOperationException(CoreStrings.EFConstantWithNonEvaluableArgument); - } - - return Evaluate(operand, generateParameter: false); - } - - case nameof(EF.Parameter): - { - var operand = methodCallExpression.Arguments[0]; - if (!_evaluatableExpressions.TryGetValue(operand, out _)) - { - throw new InvalidOperationException(CoreStrings.EFConstantWithNonEvaluableArgument); - } - - return Evaluate(operand, generateParameter: true); - } - } - } - - return base.VisitMethodCall(methodCallExpression); - } - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - protected override Expression VisitBinary(BinaryExpression binaryExpression) - { - switch (binaryExpression.NodeType) - { - case ExpressionType.Coalesce: - { - var newLeftExpression = TryGetConstantValue(binaryExpression.Left) ?? Visit(binaryExpression.Left); - if (newLeftExpression is ConstantExpression constantLeftExpression) - { - return constantLeftExpression.Value == null - ? Visit(binaryExpression.Right) - : newLeftExpression; - } - - return binaryExpression.Update( - newLeftExpression, - binaryExpression.Conversion, - Visit(binaryExpression.Right)); - } - - case ExpressionType.AndAlso: - case ExpressionType.OrElse: - { - var newLeftExpression = TryGetConstantValue(binaryExpression.Left) ?? Visit(binaryExpression.Left); - if (ShortCircuitLogicalExpression(newLeftExpression, binaryExpression.NodeType)) - { - return newLeftExpression; - } - - var newRightExpression = TryGetConstantValue(binaryExpression.Right) ?? Visit(binaryExpression.Right); - return ShortCircuitLogicalExpression(newRightExpression, binaryExpression.NodeType) - ? newRightExpression - : binaryExpression.Update(newLeftExpression, binaryExpression.Conversion, newRightExpression); - } - - default: - return base.VisitBinary(binaryExpression); - } - } - - private Expression? TryGetConstantValue(Expression expression) - { - if (_evaluatableExpressions.ContainsKey(expression)) - { - var value = GetValue(expression, out _); - - if (value is bool) - { - return Expression.Constant(value, typeof(bool)); - } - } - - return null; - } - - private static bool ShortCircuitLogicalExpression(Expression expression, ExpressionType nodeType) - => expression is ConstantExpression { Value: bool constantValue } - && ((constantValue && nodeType == ExpressionType.OrElse) - || (!constantValue && nodeType == ExpressionType.AndAlso)); - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - protected override Expression VisitExtension(Expression extensionExpression) - { - if (extensionExpression is QueryRootExpression queryRootExpression) - { - var queryProvider = queryRootExpression.QueryProvider; - if (_currentQueryProvider == null) - { - _currentQueryProvider = queryProvider; - } - else if (!ReferenceEquals(queryProvider, _currentQueryProvider)) - { - throw new InvalidOperationException(CoreStrings.ErrorInvalidQueryable); - } - - // Visit after detaching query provider since custom query roots can have additional components - extensionExpression = queryRootExpression.DetachQueryProvider(); - } - - return base.VisitExtension(extensionExpression); - } - - private static Expression GenerateConstantExpression(object? value, Type returnType) - { - var constantExpression = Expression.Constant(value, value?.GetType() ?? returnType); - - return constantExpression.Type != returnType - ? Expression.Convert(constantExpression, returnType) - : constantExpression; - } - - private Expression Evaluate(Expression expression, bool generateParameter) - { - object? parameterValue; - string? parameterName; - if (_evaluatedValues.TryGetValue(expression, out var cachedValue)) - { - // The _generateContextAccessors condition allows us to reuse parameter expressions evaluated in query filters. - // In principle, _generateContextAccessors is orthogonal to query filters, but in practice it is only used in the - // nav expansion query filters (and defining query). If this changes in future, they would need to be decoupled. - var existingExpression = generateParameter || _generateContextAccessors - ? cachedValue.Parameter - : cachedValue.Constant; - - if (existingExpression != null) - { - return existingExpression; - } - - parameterValue = cachedValue.Value; - parameterName = cachedValue.CandidateParameterName; - } - else - { - parameterValue = GetValue(expression, out parameterName); - cachedValue = new EvaluatedValues { CandidateParameterName = parameterName, Value = parameterValue }; - _evaluatedValues[expression] = cachedValue; - } - - if (parameterValue is IQueryable innerQueryable) - { - return ExtractParameters(innerQueryable.Expression, clearEvaluatedValues: false); - } - - if (parameterName?.StartsWith(QueryFilterPrefix, StringComparison.Ordinal) != true) - { - if (parameterValue is Expression innerExpression) - { - return ExtractParameters(innerExpression, clearEvaluatedValues: false); - } - - if (!generateParameter) - { - var constantValue = GenerateConstantExpression(parameterValue, expression.Type); - - cachedValue.Constant = constantValue; - - return constantValue; - } - } - - parameterName ??= "p"; - - if (string.Equals(QueryFilterPrefix, parameterName, StringComparison.Ordinal)) - { - parameterName = QueryFilterPrefix + "__p"; - } - - var compilerPrefixIndex - = parameterName.LastIndexOf(">", StringComparison.Ordinal); - - if (compilerPrefixIndex != -1) - { - parameterName = parameterName[(compilerPrefixIndex + 1)..]; - } - - parameterName - = QueryCompilationContext.QueryParameterPrefix - + parameterName - + "_" - + _parameterValues.ParameterValues.Count; - - _parameterValues.AddParameter(parameterName, parameterValue); - - var parameter = Expression.Parameter(expression.Type, parameterName); - - cachedValue.Parameter = parameter; - - return parameter; - } - - private sealed class ContextParameterReplacingExpressionVisitor : ExpressionVisitor - { - private readonly Type _contextType; - - public ContextParameterReplacingExpressionVisitor(Type contextType) - { - ContextParameterExpression = Expression.Parameter(contextType, "context"); - _contextType = contextType; - } - - public ParameterExpression ContextParameterExpression { get; } - - [return: NotNullIfNotNull("expression")] - public override Expression? Visit(Expression? expression) - => expression?.Type != typeof(object) - && expression?.Type.IsAssignableFrom(_contextType) == true - ? ContextParameterExpression - : base.Visit(expression); - } - - private static Expression RemoveConvert(Expression expression) - { - if (expression is UnaryExpression unaryExpression - && expression.NodeType is ExpressionType.Convert or ExpressionType.ConvertChecked) - { - return RemoveConvert(unaryExpression.Operand); - } - - return expression; - } - - private object? GetValue(Expression? expression, out string? parameterName) - { - parameterName = null; - - if (expression == null) - { - return null; - } - - if (_generateContextAccessors) - { - var newExpression = _contextParameterReplacingExpressionVisitor.Visit(expression); - - if (newExpression != expression) - { - if (newExpression.Type is IQueryable) - { - return newExpression; - } - - parameterName = QueryFilterPrefix - + (RemoveConvert(expression) is MemberExpression memberExpression - ? ("__" + memberExpression.Member.Name) - : ""); - - return Expression.Lambda( - newExpression, - _contextParameterReplacingExpressionVisitor.ContextParameterExpression); - } - } - - switch (expression) - { - case MemberExpression memberExpression: - var instanceValue = GetValue(memberExpression.Expression, out parameterName); - try - { - switch (memberExpression.Member) - { - case FieldInfo fieldInfo: - parameterName = (parameterName != null ? parameterName + "_" : "") + fieldInfo.Name; - return fieldInfo.GetValue(instanceValue); - - case PropertyInfo propertyInfo: - parameterName = (parameterName != null ? parameterName + "_" : "") + propertyInfo.Name; - return propertyInfo.GetValue(instanceValue); - } - } - catch - { - // Try again when we compile the delegate - } - - break; - - case ConstantExpression constantExpression: - return constantExpression.Value; - - case MethodCallExpression methodCallExpression: - parameterName = methodCallExpression.Method.Name; - break; - - case UnaryExpression { NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked } unaryExpression - when (unaryExpression.Type.UnwrapNullableType() == unaryExpression.Operand.Type): - return GetValue(unaryExpression.Operand, out parameterName); - } - - try - { - return Expression.Lambda>( - Expression.Convert(expression, typeof(object))) - .Compile(preferInterpretation: true) - .Invoke(); - } - catch (Exception exception) - { - throw new InvalidOperationException( - _logger.ShouldLogSensitiveData() - ? CoreStrings.ExpressionParameterizationExceptionSensitive(expression) - : CoreStrings.ExpressionParameterizationException, - exception); - } - } - - private sealed class EvaluatableExpressionFindingExpressionVisitor : ExpressionVisitor - { - private readonly IEvaluatableExpressionFilter _evaluatableExpressionFilter; - private readonly ISet _allowedParameters = new HashSet(); - private readonly IModel _model; - private readonly bool _parameterize; - - private bool _evaluatable; - private bool _containsClosure; - private bool _inLambda; - private IDictionary _evaluatableExpressions; - - public EvaluatableExpressionFindingExpressionVisitor( - IEvaluatableExpressionFilter evaluatableExpressionFilter, - IModel model, - bool parameterize) - { - _evaluatableExpressionFilter = evaluatableExpressionFilter; - _model = model; - _parameterize = parameterize; - // The entry method will take care of populating this field always. So accesses should be safe. - _evaluatableExpressions = null!; - } - - public IDictionary Find(Expression expression) - { - _evaluatable = true; - _containsClosure = false; - _inLambda = false; - _evaluatableExpressions = new Dictionary(); - _allowedParameters.Clear(); - - Visit(expression); - - return _evaluatableExpressions; - } - - [return: NotNullIfNotNull("expression")] - public override Expression? Visit(Expression? expression) - { - if (expression == null) - { - return base.Visit(expression); - } - - var parentEvaluatable = _evaluatable; - var parentContainsClosure = _containsClosure; - - _evaluatable = IsEvaluatableNodeType(expression, out var preferNoEvaluation) - // Extension point to disable funcletization - && _evaluatableExpressionFilter.IsEvaluatableExpression(expression, _model) - // Don't evaluate QueryableMethods if in compiled query - && (_parameterize || !IsQueryableMethod(expression)); - _containsClosure = false; - - base.Visit(expression); - - if (_evaluatable && !preferNoEvaluation) - { - // Force parameterization when not in lambda - _evaluatableExpressions[expression] = _containsClosure || !_inLambda; - } - - _evaluatable = parentEvaluatable && _evaluatable; - _containsClosure = parentContainsClosure || _containsClosure; - - return expression; - } - - protected override Expression VisitLambda(Expression lambdaExpression) - { - var oldInLambda = _inLambda; - _inLambda = true; - - // Note: Don't skip visiting parameter here. - // SelectMany does not use parameter in lambda but we should still block it from evaluating - base.VisitLambda(lambdaExpression); - - _inLambda = oldInLambda; - return lambdaExpression; - } - - protected override Expression VisitMemberInit(MemberInitExpression memberInitExpression) - { - Visit(memberInitExpression.Bindings, VisitMemberBinding); - - // Cannot make parameter for NewExpression if Bindings cannot be evaluated - // but we still need to visit inside of it. - var bindingsEvaluatable = _evaluatable; - Visit(memberInitExpression.NewExpression); - - if (!bindingsEvaluatable) - { - _evaluatableExpressions.Remove(memberInitExpression.NewExpression); - } - - return memberInitExpression; - } - - protected override Expression VisitListInit(ListInitExpression listInitExpression) - { - Visit(listInitExpression.Initializers, VisitElementInit); - - // Cannot make parameter for NewExpression if Initializers cannot be evaluated - // but we still need to visit inside of it. - var initializersEvaluatable = _evaluatable; - Visit(listInitExpression.NewExpression); - - if (!initializersEvaluatable) - { - _evaluatableExpressions.Remove(listInitExpression.NewExpression); - } - - return listInitExpression; - } - - protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression) - { - Visit(methodCallExpression.Object); - var parameterInfos = methodCallExpression.Method.GetParameters(); - for (var i = 0; i < methodCallExpression.Arguments.Count; i++) - { - if (i == 1 - && _evaluatableExpressions.ContainsKey(methodCallExpression.Arguments[0]) - && methodCallExpression.Method.DeclaringType == typeof(Enumerable) - && methodCallExpression.Method.Name == nameof(Enumerable.Select) - && methodCallExpression.Arguments[1] is LambdaExpression lambdaExpression) - { - // Allow evaluation Enumerable.Select operation - foreach (var parameter in lambdaExpression.Parameters) - { - _allowedParameters.Add(parameter); - } - } - - Visit(methodCallExpression.Arguments[i]); - - if (_evaluatableExpressions.ContainsKey(methodCallExpression.Arguments[i]) - && (parameterInfos[i].GetCustomAttribute() != null - || _model.IsIndexerMethod(methodCallExpression.Method))) - { - _evaluatableExpressions[methodCallExpression.Arguments[i]] = false; - } - } - - return methodCallExpression; - } - - protected override Expression VisitMember(MemberExpression memberExpression) - { - _containsClosure = memberExpression.Expression != null - || !(memberExpression.Member is FieldInfo { IsInitOnly: true }); - return base.VisitMember(memberExpression); - } - - protected override Expression VisitParameter(ParameterExpression parameterExpression) - { - _evaluatable = _allowedParameters.Contains(parameterExpression); - - return base.VisitParameter(parameterExpression); - } - - protected override Expression VisitConstant(ConstantExpression constantExpression) - { - _evaluatable = !(constantExpression.Value is IQueryable); - -#pragma warning disable RCS1096 // Use bitwise operation instead of calling 'HasFlag'. - _containsClosure - = (constantExpression.Type.Attributes.HasFlag(TypeAttributes.NestedPrivate) - && Attribute.IsDefined(constantExpression.Type, typeof(CompilerGeneratedAttribute), inherit: true)) // Closure - || constantExpression.Type == typeof(ValueBuffer); // Find method -#pragma warning restore RCS1096 // Use bitwise operation instead of calling 'HasFlag'. - - return base.VisitConstant(constantExpression); - } - - private static bool IsEvaluatableNodeType(Expression expression, out bool preferNoEvaluation) - { - switch (expression.NodeType) - { - case ExpressionType.NewArrayInit: - preferNoEvaluation = true; - return true; - - case ExpressionType.Extension: - preferNoEvaluation = false; - return expression.CanReduce && IsEvaluatableNodeType(expression.ReduceAndCheck(), out preferNoEvaluation); - - // Identify a call to EF.Constant(), and flag that as non-evaluable. - // This is important to prevent a larger subtree containing EF.Constant from being evaluated, i.e. to make sure that - // the EF.Function argument is present in the tree as its own, constant node. - case ExpressionType.Call - when expression is MethodCallExpression { Method: var method } - && method.DeclaringType == typeof(EF) - && method.Name is nameof(EF.Constant) or nameof(EF.Parameter): - preferNoEvaluation = true; - return false; - - default: - preferNoEvaluation = false; - return true; - } - } - - private static bool IsQueryableMethod(Expression expression) - => expression is MethodCallExpression methodCallExpression - && methodCallExpression.Method.DeclaringType == typeof(Queryable); - } - - private sealed class EvaluatedValues - { - public string? CandidateParameterName { get; init; } - public object? Value { get; init; } - public Expression? Constant { get; set; } - public Expression? Parameter { get; set; } - } -} diff --git a/src/EFCore/Query/Internal/QueryCompiler.cs b/src/EFCore/Query/Internal/QueryCompiler.cs index 441f0fd88fc..a0536aff1bb 100644 --- a/src/EFCore/Query/Internal/QueryCompiler.cs +++ b/src/EFCore/Query/Internal/QueryCompiler.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Runtime.CompilerServices; + namespace Microsoft.EntityFrameworkCore.Query.Internal; /// @@ -54,33 +56,36 @@ public QueryCompiler( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual TResult Execute(Expression query) + => ExecuteCore(query, async: false, CancellationToken.None); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual TResult ExecuteAsync(Expression query, CancellationToken cancellationToken = default) + => ExecuteCore(query, async: true, cancellationToken); + + private TResult ExecuteCore(Expression query, bool async, CancellationToken cancellationToken) { var queryContext = _queryContextFactory.Create(); - query = ExtractParameters(query, queryContext, _logger); + queryContext.CancellationToken = cancellationToken; + + var queryAfterExtraction = ExtractParameters(query, queryContext, _logger); var compiledQuery = _compiledQueryCache .GetOrAddQuery( - _compiledQueryCacheKeyGenerator.GenerateCacheKey(query, async: false), - () => CompileQueryCore(_database, query, _model, false)); + _compiledQueryCacheKeyGenerator.GenerateCacheKey(queryAfterExtraction, async), + () => RuntimeFeature.IsDynamicCodeSupported + ? CompileQueryCore(_database, queryAfterExtraction, _model, async) + : throw new InvalidOperationException("Query wasn't precompiled and dynamic code isn't supported (NativeAOT)")); return compiledQuery(queryContext); } - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public virtual Func CompileQueryCore( - IDatabase database, - Expression query, - IModel model, - bool async) - => database.CompileQuery(query, async); - /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -89,9 +94,9 @@ public virtual Func CompileQueryCore( /// public virtual Func CreateCompiledQuery(Expression query) { - query = ExtractParameters(query, _queryContextFactory.Create(), _logger, parameterize: false); + var queryAfterExtraction = ExtractParameters(query, _queryContextFactory.Create(), _logger, compiledQuery: true); - return CompileQueryCore(_database, query, _model, false); + return CompileQueryCore(_database, queryAfterExtraction, _model, false); } /// @@ -100,21 +105,11 @@ public virtual Func CreateCompiledQuery(Expressi /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual TResult ExecuteAsync(Expression query, CancellationToken cancellationToken = default) + public virtual Func CreateCompiledAsyncQuery(Expression query) { - var queryContext = _queryContextFactory.Create(); - - queryContext.CancellationToken = cancellationToken; + var queryAfterExtraction = ExtractParameters(query, _queryContextFactory.Create(), _logger, compiledQuery: true); - query = ExtractParameters(query, queryContext, _logger); - - var compiledQuery - = _compiledQueryCache - .GetOrAddQuery( - _compiledQueryCacheKeyGenerator.GenerateCacheKey(query, async: true), - () => CompileQueryCore(_database, query, _model, true)); - - return compiledQuery(queryContext); + return CompileQueryCore(_database, queryAfterExtraction, _model, true); } /// @@ -123,12 +118,12 @@ var compiledQuery /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual Func CreateCompiledAsyncQuery(Expression query) - { - query = ExtractParameters(query, _queryContextFactory.Create(), _logger, parameterize: false); - - return CompileQueryCore(_database, query, _model, true); - } + public virtual Func CompileQueryCore( + IDatabase database, + Expression query, + IModel model, + bool async) + => database.CompileQuery(query, async); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -140,18 +135,8 @@ public virtual Expression ExtractParameters( Expression query, IParameterValues parameterValues, IDiagnosticsLogger logger, - bool parameterize = true, + bool compiledQuery = false, bool generateContextAccessors = false) - { - var visitor = new ParameterExtractingExpressionVisitor( - _evaluatableExpressionFilter, - parameterValues, - _contextType, - _model, - logger, - parameterize, - generateContextAccessors); - - return visitor.ExtractParameters(query); - } + => new ExpressionTreeFuncletizer(_model, _evaluatableExpressionFilter, _contextType, generateContextAccessors: false, logger) + .ExtractParameters(query, parameterValues, parameterize: !compiledQuery, clearParameterizedValues: true); } diff --git a/test/EFCore.Specification.Tests/Query/Ef6GroupByTestBase.cs b/test/EFCore.Specification.Tests/Query/Ef6GroupByTestBase.cs index 74d60093205..304de28685e 100644 --- a/test/EFCore.Specification.Tests/Query/Ef6GroupByTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/Ef6GroupByTestBase.cs @@ -77,7 +77,7 @@ public virtual Task GroupBy_is_optimized_when_projecting_conditional_expression_ [ConditionalTheory] [MemberData(nameof(IsAsyncData))] - public virtual Task GroupBy_is_optimized_when_filerting_and_projecting_anonymous_type_with_group_key_and_function_aggregate( + public virtual Task GroupBy_is_optimized_when_filtering_and_projecting_anonymous_type_with_group_key_and_function_aggregate( bool async) => AssertQuery( async, diff --git a/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs index ec3fb71e405..1eb9030b952 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs @@ -2855,6 +2855,16 @@ public virtual void Can_cast_CreateQuery_result_to_IQueryable_T_bug_1730() products = (IQueryable)products.Provider.CreateQuery(products.Expression); } + [ConditionalFact] + public virtual async Task IQueryable_captured_variable() + { + await using var context = CreateContext(); + + IQueryable nestedOrdersQuery = context.Orders; + + _ = await context.Customers.CountAsync(c => nestedOrdersQuery.Count() == 2); + } + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Select_Subquery_Single(bool async) diff --git a/test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs index 02583323e23..3364813cc05 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs @@ -1211,11 +1211,19 @@ await AssertQuery( ss => ss.Set().Where(c => c.CustomerID == "ALFKI" && boolean), assertEmpty: true); + await AssertQuery( + async, + ss => ss.Set().Where(c => c.CustomerID == "ALFKI" || boolean)); + boolean = true; await AssertQuery( async, ss => ss.Set().Where(c => c.CustomerID == "ALFKI" && boolean)); + + await AssertQuery( + async, + ss => ss.Set().Where(c => c.CustomerID == "ALFKI" || boolean)); } [ConditionalTheory] @@ -2391,7 +2399,7 @@ public virtual async Task EF_Constant_with_non_evaluatable_argument_throws(bool async, ss => ss.Set().Where(c => c.CustomerID == EF.Constant(c.CustomerID)))); - Assert.Equal(CoreStrings.EFConstantWithNonEvaluableArgument, exception.Message); + Assert.Equal(CoreStrings.EFConstantWithNonEvaluatableArgument, exception.Message); } [ConditionalTheory] @@ -2438,7 +2446,7 @@ public virtual async Task EF_Parameter_with_non_evaluatable_argument_throws(bool async, ss => ss.Set().Where(c => c.CustomerID == EF.Parameter(c.CustomerID)))); - Assert.Equal(CoreStrings.EFConstantWithNonEvaluableArgument, exception.Message); + Assert.Equal(CoreStrings.EFParameterWithNonEvaluatableArgument, exception.Message); } private class EntityWithImplicitCast(int value) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Ef6GroupBySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Ef6GroupBySqlServerTest.cs index 9aa64119a51..6df6a492fc6 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Ef6GroupBySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Ef6GroupBySqlServerTest.cs @@ -159,12 +159,10 @@ public override async Task GroupBy_is_optimized_when_projecting_conditional_expr AssertSql( """ -@__p_0='False' - SELECT CASE WHEN [a].[FirstName] IS NULL THEN N'is null' ELSE N'not null' -END AS [keyIsNull], @__p_0 AS [logicExpression] +END AS [keyIsNull], CAST(0 AS bit) AS [logicExpression] FROM [ArubaOwner] AS [a] GROUP BY [a].[FirstName] """); @@ -180,10 +178,10 @@ GROUP BY [a].[FirstName] // ) AS [Distinct1]"; } - public override async Task GroupBy_is_optimized_when_filerting_and_projecting_anonymous_type_with_group_key_and_function_aggregate( + public override async Task GroupBy_is_optimized_when_filtering_and_projecting_anonymous_type_with_group_key_and_function_aggregate( bool async) { - await base.GroupBy_is_optimized_when_filerting_and_projecting_anonymous_type_with_group_key_and_function_aggregate(async); + await base.GroupBy_is_optimized_when_filtering_and_projecting_anonymous_type_with_group_key_and_function_aggregate(async); AssertSql( """ diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs index 13d6165a6b7..beb1680481e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/GearsOfWarQuerySqlServerTest.cs @@ -10374,40 +10374,40 @@ public override async Task Nested_contains_with_enum(bool async) AssertSql( """ -@__ranks_1='[1]' (Size = 4000) -@__key_2='5f221fb9-66f4-442a-92c9-d97ed5989cc7' -@__keys_0='["0a47bcb7-a1cb-4345-8944-c58f82d6aac7","5f221fb9-66f4-442a-92c9-d97ed5989cc7"]' (Size = 4000) +@__ranks_0='[1]' (Size = 4000) +@__key_1='5f221fb9-66f4-442a-92c9-d97ed5989cc7' +@__keys_2='["0a47bcb7-a1cb-4345-8944-c58f82d6aac7","5f221fb9-66f4-442a-92c9-d97ed5989cc7"]' (Size = 4000) SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank] FROM [Gears] AS [g] WHERE CASE WHEN [g].[Rank] IN ( SELECT [r].[value] - FROM OPENJSON(@__ranks_1) WITH ([value] int '$') AS [r] - ) THEN @__key_2 - ELSE @__key_2 + FROM OPENJSON(@__ranks_0) WITH ([value] int '$') AS [r] + ) THEN @__key_1 + ELSE @__key_1 END IN ( SELECT [k].[value] - FROM OPENJSON(@__keys_0) WITH ([value] uniqueidentifier '$') AS [k] + FROM OPENJSON(@__keys_2) WITH ([value] uniqueidentifier '$') AS [k] ) """, // """ -@__ammoTypes_1='[1]' (Size = 4000) -@__key_2='5f221fb9-66f4-442a-92c9-d97ed5989cc7' -@__keys_0='["0a47bcb7-a1cb-4345-8944-c58f82d6aac7","5f221fb9-66f4-442a-92c9-d97ed5989cc7"]' (Size = 4000) +@__ammoTypes_0='[1]' (Size = 4000) +@__key_1='5f221fb9-66f4-442a-92c9-d97ed5989cc7' +@__keys_2='["0a47bcb7-a1cb-4345-8944-c58f82d6aac7","5f221fb9-66f4-442a-92c9-d97ed5989cc7"]' (Size = 4000) SELECT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] FROM [Weapons] AS [w] WHERE CASE WHEN [w].[AmmunitionType] IN ( SELECT [a].[value] - FROM OPENJSON(@__ammoTypes_1) WITH ([value] int '$') AS [a] - ) THEN @__key_2 - ELSE @__key_2 + FROM OPENJSON(@__ammoTypes_0) WITH ([value] int '$') AS [a] + ) THEN @__key_1 + ELSE @__key_1 END IN ( SELECT [k].[value] - FROM OPENJSON(@__keys_0) WITH ([value] uniqueidentifier '$') AS [k] + FROM OPENJSON(@__keys_2) WITH ([value] uniqueidentifier '$') AS [k] ) """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindDbFunctionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindDbFunctionsQuerySqlServerTest.cs index 2f44080e1c5..c9561c43dfb 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindDbFunctionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindDbFunctionsQuerySqlServerTest.cs @@ -986,17 +986,17 @@ await AssertCount( AssertSql( """ -@__dateTime_0='1919-12-12T10:20:15.0000000' (DbType = DateTime) -@__dateTime_Month_2='12' -@__dateTime_Day_3='12' -@__dateTime_Hour_4='10' -@__dateTime_Minute_5='20' -@__dateTime_Second_6='15' -@__dateTime_Millisecond_7='0' +@__dateTime_7='1919-12-12T10:20:15.0000000' (DbType = DateTime) +@__dateTime_Month_1='12' +@__dateTime_Day_2='12' +@__dateTime_Hour_3='10' +@__dateTime_Minute_4='20' +@__dateTime_Second_5='15' +@__dateTime_Millisecond_6='0' SELECT COUNT(*) FROM [Orders] AS [o] -WHERE @__dateTime_0 > DATETIMEFROMPARTS(DATEPART(year, GETDATE()), @__dateTime_Month_2, @__dateTime_Day_3, @__dateTime_Hour_4, @__dateTime_Minute_5, @__dateTime_Second_6, @__dateTime_Millisecond_7) +WHERE @__dateTime_7 > DATETIMEFROMPARTS(DATEPART(year, GETDATE()), @__dateTime_Month_1, @__dateTime_Day_2, @__dateTime_Hour_3, @__dateTime_Minute_4, @__dateTime_Second_5, @__dateTime_Millisecond_6) """); } @@ -1052,13 +1052,13 @@ await AssertCount( AssertSql( """ -@__date_0='1919-12-12T00:00:00.0000000' (DbType = Date) -@__date_Month_2='12' -@__date_Day_3='12' +@__date_3='1919-12-12T00:00:00.0000000' (DbType = Date) +@__date_Month_1='12' +@__date_Day_2='12' SELECT COUNT(*) FROM [Orders] AS [o] -WHERE @__date_0 > DATEFROMPARTS(DATEPART(year, GETDATE()), @__date_Month_2, @__date_Day_3) +WHERE @__date_3 > DATEFROMPARTS(DATEPART(year, GETDATE()), @__date_Month_1, @__date_Day_2) """); } @@ -1120,17 +1120,17 @@ public virtual void DateTime2FromParts_compare_with_local_variable() AssertSql( """ -@__dateTime_0='1919-12-12T10:20:15.0000000' -@__dateTime_Month_2='12' -@__dateTime_Day_3='12' -@__dateTime_Hour_4='10' -@__dateTime_Minute_5='20' -@__dateTime_Second_6='15' -@__fractions_7='9999999' +@__dateTime_7='1919-12-12T10:20:15.0000000' +@__dateTime_Month_1='12' +@__dateTime_Day_2='12' +@__dateTime_Hour_3='10' +@__dateTime_Minute_4='20' +@__dateTime_Second_5='15' +@__fractions_6='9999999' SELECT COUNT(*) FROM [Orders] AS [o] -WHERE @__dateTime_0 > DATETIME2FROMPARTS(DATEPART(year, GETDATE()), @__dateTime_Month_2, @__dateTime_Day_3, @__dateTime_Hour_4, @__dateTime_Minute_5, @__dateTime_Second_6, @__fractions_7, 7) +WHERE @__dateTime_7 > DATETIME2FROMPARTS(DATEPART(year, GETDATE()), @__dateTime_Month_1, @__dateTime_Day_2, @__dateTime_Hour_3, @__dateTime_Minute_4, @__dateTime_Second_5, @__fractions_6, 7) """); } } @@ -1195,19 +1195,19 @@ public virtual void DateTimeOffsetFromParts_compare_with_local_variable() AssertSql( """ -@__dateTimeOffset_0='1919-12-12T10:20:15.0000000+01:30' -@__dateTimeOffset_Month_2='12' -@__dateTimeOffset_Day_3='12' -@__dateTimeOffset_Hour_4='10' -@__dateTimeOffset_Minute_5='20' -@__dateTimeOffset_Second_6='15' -@__fractions_7='5' -@__hourOffset_8='1' -@__minuteOffset_9='30' +@__dateTimeOffset_9='1919-12-12T10:20:15.0000000+01:30' +@__dateTimeOffset_Month_1='12' +@__dateTimeOffset_Day_2='12' +@__dateTimeOffset_Hour_3='10' +@__dateTimeOffset_Minute_4='20' +@__dateTimeOffset_Second_5='15' +@__fractions_6='5' +@__hourOffset_7='1' +@__minuteOffset_8='30' SELECT COUNT(*) FROM [Orders] AS [o] -WHERE @__dateTimeOffset_0 > DATETIMEOFFSETFROMPARTS(DATEPART(year, GETDATE()), @__dateTimeOffset_Month_2, @__dateTimeOffset_Day_3, @__dateTimeOffset_Hour_4, @__dateTimeOffset_Minute_5, @__dateTimeOffset_Second_6, @__fractions_7, @__hourOffset_8, @__minuteOffset_9, 7) +WHERE @__dateTimeOffset_9 > DATETIMEOFFSETFROMPARTS(DATEPART(year, GETDATE()), @__dateTimeOffset_Month_1, @__dateTimeOffset_Day_2, @__dateTimeOffset_Hour_3, @__dateTimeOffset_Minute_4, @__dateTimeOffset_Second_5, @__fractions_6, @__hourOffset_7, @__minuteOffset_8, 7) """); } } @@ -1265,15 +1265,15 @@ await AssertCount( AssertSql( """ -@__dateTime_0='1919-12-12T23:20:00.0000000' (DbType = DateTime) -@__dateTime_Month_2='12' -@__dateTime_Day_3='12' -@__dateTime_Hour_4='23' -@__dateTime_Minute_5='20' +@__dateTime_5='1919-12-12T23:20:00.0000000' (DbType = DateTime) +@__dateTime_Month_1='12' +@__dateTime_Day_2='12' +@__dateTime_Hour_3='23' +@__dateTime_Minute_4='20' SELECT COUNT(*) FROM [Orders] AS [o] -WHERE @__dateTime_0 > SMALLDATETIMEFROMPARTS(DATEPART(year, GETDATE()), @__dateTime_Month_2, @__dateTime_Day_3, @__dateTime_Hour_4, @__dateTime_Minute_5) +WHERE @__dateTime_5 > SMALLDATETIMEFROMPARTS(DATEPART(year, GETDATE()), @__dateTime_Month_1, @__dateTime_Day_2, @__dateTime_Hour_3, @__dateTime_Minute_4) """); } @@ -1364,11 +1364,11 @@ public virtual void DataLength_compare_with_local_variable() AssertSql( """ -@__lenght_0='100' (Nullable = true) +@__lenght_1='100' (Nullable = true) SELECT COUNT(*) FROM [Orders] AS [o] -WHERE @__lenght_0 < DATALENGTH([o].[OrderDate]) +WHERE @__lenght_1 < DATALENGTH([o].[OrderDate]) """); } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs index f8d5bccc73d..b1476c07a6b 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs @@ -1174,11 +1174,7 @@ public override async Task Ternary_should_not_evaluate_both_sides(bool async) AssertSql( """ -@__p_0='none' (Size = 4000) -@__p_1='none' (Size = 4000) -@__p_2='none' (Size = 4000) - -SELECT [c].[CustomerID], @__p_0 AS [Data1], @__p_1 AS [Data2], @__p_2 AS [Data3] +SELECT [c].[CustomerID], N'none' AS [Data1] FROM [Customers] AS [c] """); } @@ -2801,9 +2797,7 @@ public override async Task Null_Coalesce_Short_Circuit(bool async) AssertSql( """ -@__p_0='False' - -SELECT [c0].[CustomerID], [c0].[Address], [c0].[City], [c0].[CompanyName], [c0].[ContactName], [c0].[ContactTitle], [c0].[Country], [c0].[Fax], [c0].[Phone], [c0].[PostalCode], [c0].[Region], @__p_0 AS [Test] +SELECT [c0].[CustomerID], [c0].[Address], [c0].[City], [c0].[CompanyName], [c0].[ContactName], [c0].[ContactTitle], [c0].[Country], [c0].[Fax], [c0].[Phone], [c0].[PostalCode], [c0].[Region], CAST(0 AS bit) AS [Test] FROM ( SELECT DISTINCT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] @@ -5626,11 +5620,11 @@ public override async Task Entity_equality_with_null_coalesce_client_side(bool a AssertSql( """ -@__entity_equality_p_0_CustomerID='ALFKI' (Size = 5) (DbType = StringFixedLength) +@__entity_equality_a_0_CustomerID='ALFKI' (Size = 5) (DbType = StringFixedLength) SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE [c].[CustomerID] = @__entity_equality_p_0_CustomerID +WHERE [c].[CustomerID] = @__entity_equality_a_0_CustomerID """); } @@ -7131,6 +7125,20 @@ public override void Can_cast_CreateQuery_result_to_IQueryable_T_bug_1730() AssertSql(); } + public override async Task IQueryable_captured_variable() + { + await base.IQueryable_captured_variable(); + + AssertSql( + """ +SELECT COUNT(*) +FROM [Customers] AS [c] +WHERE ( + SELECT COUNT(*) + FROM [Orders] AS [o]) = 2 +"""); + } + public override async Task Multiple_context_instances(bool async) { await base.Multiple_context_instances(async); @@ -7407,14 +7415,14 @@ public override async Task Contains_over_concatenated_column_and_parameter(bool AssertSql( """ -@__someVariable_1='SomeVariable' (Size = 4000) -@__data_0='["ALFKISomeVariable","ANATRSomeVariable","ALFKIX"]' (Size = 4000) +@__someVariable_0='SomeVariable' (Size = 4000) +@__data_1='["ALFKISomeVariable","ANATRSomeVariable","ALFKIX"]' (Size = 4000) SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] -WHERE [c].[CustomerID] + @__someVariable_1 IN ( +WHERE [c].[CustomerID] + @__someVariable_0 IN ( SELECT [d].[value] - FROM OPENJSON(@__data_0) WITH ([value] nvarchar(max) '$') AS [d] + FROM OPENJSON(@__data_1) WITH ([value] nvarchar(max) '$') AS [d] ) """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs index 7efc8aa0923..86cccb978c6 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs @@ -3129,6 +3129,17 @@ FROM [Customers] AS [c] SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] FROM [Customers] AS [c] WHERE [c].[CustomerID] = N'ALFKI' +""", + // + """ +SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] +FROM [Customers] AS [c] +WHERE [c].[CustomerID] = N'ALFKI' +""", + // + """ +SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] +FROM [Customers] AS [c] """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs index db080f82a63..b9970df7564 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs @@ -503,27 +503,24 @@ public override async Task Preserve_includes_when_applying_skip_take_after_anony AssertSql( """ -SELECT COUNT(*) -FROM [OwnedPerson] AS [o] -""", - // - """ -@__p_1='0' -@__p_2='100' +@__p_0='0' +@__p_1='100' -SELECT [o2].[Id], [o2].[Discriminator], [o2].[Name], [s].[ClientId], [s].[Id], [s].[OrderDate], [s].[OrderClientId], [s].[OrderId], [s].[Id0], [s].[Detail], [o2].[PersonAddress_AddressLine], [o2].[PersonAddress_PlaceType], [o2].[PersonAddress_ZipCode], [o2].[PersonAddress_Country_Name], [o2].[PersonAddress_Country_PlanetId], [o2].[BranchAddress_BranchName], [o2].[BranchAddress_PlaceType], [o2].[BranchAddress_Country_Name], [o2].[BranchAddress_Country_PlanetId], [o2].[LeafBAddress_LeafBType], [o2].[LeafBAddress_PlaceType], [o2].[LeafBAddress_Country_Name], [o2].[LeafBAddress_Country_PlanetId], [o2].[LeafAAddress_LeafType], [o2].[LeafAAddress_PlaceType], [o2].[LeafAAddress_Country_Name], [o2].[LeafAAddress_Country_PlanetId] +SELECT [o3].[Id], [o3].[Discriminator], [o3].[Name], [s].[ClientId], [s].[Id], [s].[OrderDate], [s].[OrderClientId], [s].[OrderId], [s].[Id0], [s].[Detail], [o3].[PersonAddress_AddressLine], [o3].[PersonAddress_PlaceType], [o3].[PersonAddress_ZipCode], [o3].[PersonAddress_Country_Name], [o3].[PersonAddress_Country_PlanetId], [o3].[BranchAddress_BranchName], [o3].[BranchAddress_PlaceType], [o3].[BranchAddress_Country_Name], [o3].[BranchAddress_Country_PlanetId], [o3].[LeafBAddress_LeafBType], [o3].[LeafBAddress_PlaceType], [o3].[LeafBAddress_Country_Name], [o3].[LeafBAddress_Country_PlanetId], [o3].[LeafAAddress_LeafType], [o3].[LeafAAddress_PlaceType], [o3].[LeafAAddress_Country_Name], [o3].[LeafAAddress_Country_PlanetId], [o3].[c] FROM ( - SELECT [o].[Id], [o].[Discriminator], [o].[Name], [o].[PersonAddress_AddressLine], [o].[PersonAddress_PlaceType], [o].[PersonAddress_ZipCode], [o].[PersonAddress_Country_Name], [o].[PersonAddress_Country_PlanetId], [o].[BranchAddress_BranchName], [o].[BranchAddress_PlaceType], [o].[BranchAddress_Country_Name], [o].[BranchAddress_Country_PlanetId], [o].[LeafBAddress_LeafBType], [o].[LeafBAddress_PlaceType], [o].[LeafBAddress_Country_Name], [o].[LeafBAddress_Country_PlanetId], [o].[LeafAAddress_LeafType], [o].[LeafAAddress_PlaceType], [o].[LeafAAddress_Country_Name], [o].[LeafAAddress_Country_PlanetId] + SELECT [o].[Id], [o].[Discriminator], [o].[Name], [o].[PersonAddress_AddressLine], [o].[PersonAddress_PlaceType], [o].[PersonAddress_ZipCode], [o].[PersonAddress_Country_Name], [o].[PersonAddress_Country_PlanetId], [o].[BranchAddress_BranchName], [o].[BranchAddress_PlaceType], [o].[BranchAddress_Country_Name], [o].[BranchAddress_Country_PlanetId], [o].[LeafBAddress_LeafBType], [o].[LeafBAddress_PlaceType], [o].[LeafBAddress_Country_Name], [o].[LeafBAddress_Country_PlanetId], [o].[LeafAAddress_LeafType], [o].[LeafAAddress_PlaceType], [o].[LeafAAddress_Country_Name], [o].[LeafAAddress_Country_PlanetId], ( + SELECT COUNT(*) + FROM [OwnedPerson] AS [o2]) AS [c] FROM [OwnedPerson] AS [o] ORDER BY [o].[Id] - OFFSET @__p_1 ROWS FETCH NEXT @__p_2 ROWS ONLY -) AS [o2] + OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY +) AS [o3] LEFT JOIN ( SELECT [o0].[ClientId], [o0].[Id], [o0].[OrderDate], [o1].[OrderClientId], [o1].[OrderId], [o1].[Id] AS [Id0], [o1].[Detail] FROM [Order] AS [o0] LEFT JOIN [OrderDetail] AS [o1] ON [o0].[ClientId] = [o1].[OrderClientId] AND [o0].[Id] = [o1].[OrderId] -) AS [s] ON [o2].[Id] = [s].[ClientId] -ORDER BY [o2].[Id], [s].[ClientId], [s].[Id], [s].[OrderClientId], [s].[OrderId] +) AS [s] ON [o3].[Id] = [s].[ClientId] +ORDER BY [o3].[Id], [s].[ClientId], [s].[Id], [s].[OrderClientId], [s].[OrderId] """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs index 5e204386f5d..e7fe2079c22 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs @@ -1478,20 +1478,20 @@ public override async Task Nested_contains_with_Lists_and_no_inferred_type_mappi AssertSql( """ -@__ints_1='[1,2,3]' (Size = 4000) -@__strings_0='["one","two","three"]' (Size = 4000) +@__ints_0='[1,2,3]' (Size = 4000) +@__strings_1='["one","two","three"]' (Size = 4000) SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] FROM [PrimitiveCollectionsEntity] AS [p] WHERE CASE WHEN [p].[Int] IN ( SELECT [i].[value] - FROM OPENJSON(@__ints_1) WITH ([value] int '$') AS [i] + FROM OPENJSON(@__ints_0) WITH ([value] int '$') AS [i] ) THEN N'one' ELSE N'two' END IN ( SELECT [s].[value] - FROM OPENJSON(@__strings_0) WITH ([value] nvarchar(max) '$') AS [s] + FROM OPENJSON(@__strings_1) WITH ([value] nvarchar(max) '$') AS [s] ) """); } @@ -1502,20 +1502,20 @@ public override async Task Nested_contains_with_arrays_and_no_inferred_type_mapp AssertSql( """ -@__ints_1='[1,2,3]' (Size = 4000) -@__strings_0='["one","two","three"]' (Size = 4000) +@__ints_0='[1,2,3]' (Size = 4000) +@__strings_1='["one","two","three"]' (Size = 4000) SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] FROM [PrimitiveCollectionsEntity] AS [p] WHERE CASE WHEN [p].[Int] IN ( SELECT [i].[value] - FROM OPENJSON(@__ints_1) WITH ([value] int '$') AS [i] + FROM OPENJSON(@__ints_0) WITH ([value] int '$') AS [i] ) THEN N'one' ELSE N'two' END IN ( SELECT [s].[value] - FROM OPENJSON(@__strings_0) WITH ([value] nvarchar(max) '$') AS [s] + FROM OPENJSON(@__strings_1) WITH ([value] nvarchar(max) '$') AS [s] ) """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/QueryFilterFuncletizationSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/QueryFilterFuncletizationSqlServerTest.cs index f31ad32c55e..eb1b12e52e1 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/QueryFilterFuncletizationSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/QueryFilterFuncletizationSqlServerTest.cs @@ -225,29 +225,29 @@ public override void DbContext_property_based_filter_does_not_short_circuit() AssertSql( """ -@__ef_filter__p_0='False' -@__ef_filter__IsModerated_1='True' (Nullable = true) +@__ef_filter__p_1='False' +@__ef_filter__IsModerated_0='True' (Nullable = true) SELECT [s].[Id], [s].[IsDeleted], [s].[IsModerated] FROM [ShortCircuitFilter] AS [s] -WHERE [s].[IsDeleted] = CAST(0 AS bit) AND (@__ef_filter__p_0 = CAST(1 AS bit) OR @__ef_filter__IsModerated_1 = [s].[IsModerated]) +WHERE [s].[IsDeleted] = CAST(0 AS bit) AND (@__ef_filter__p_1 = CAST(1 AS bit) OR @__ef_filter__IsModerated_0 = [s].[IsModerated]) """, // """ -@__ef_filter__p_0='False' -@__ef_filter__IsModerated_1='False' (Nullable = true) +@__ef_filter__p_1='False' +@__ef_filter__IsModerated_0='False' (Nullable = true) SELECT [s].[Id], [s].[IsDeleted], [s].[IsModerated] FROM [ShortCircuitFilter] AS [s] -WHERE [s].[IsDeleted] = CAST(0 AS bit) AND (@__ef_filter__p_0 = CAST(1 AS bit) OR @__ef_filter__IsModerated_1 = [s].[IsModerated]) +WHERE [s].[IsDeleted] = CAST(0 AS bit) AND (@__ef_filter__p_1 = CAST(1 AS bit) OR @__ef_filter__IsModerated_0 = [s].[IsModerated]) """, // """ -@__ef_filter__p_0='True' +@__ef_filter__p_1='True' SELECT [s].[Id], [s].[IsDeleted], [s].[IsModerated] FROM [ShortCircuitFilter] AS [s] -WHERE [s].[IsDeleted] = CAST(0 AS bit) AND @__ef_filter__p_0 = CAST(1 AS bit) +WHERE [s].[IsDeleted] = CAST(0 AS bit) AND @__ef_filter__p_1 = CAST(1 AS bit) """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs index 143c3932380..0fc343b9afc 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPCGearsOfWarQuerySqlServerTest.cs @@ -13692,9 +13692,9 @@ public override async Task Nested_contains_with_enum(bool async) AssertSql( """ -@__ranks_1='[1]' (Size = 4000) -@__key_2='5f221fb9-66f4-442a-92c9-d97ed5989cc7' -@__keys_0='["0a47bcb7-a1cb-4345-8944-c58f82d6aac7","5f221fb9-66f4-442a-92c9-d97ed5989cc7"]' (Size = 4000) +@__ranks_0='[1]' (Size = 4000) +@__key_1='5f221fb9-66f4-442a-92c9-d97ed5989cc7' +@__keys_2='["0a47bcb7-a1cb-4345-8944-c58f82d6aac7","5f221fb9-66f4-442a-92c9-d97ed5989cc7"]' (Size = 4000) SELECT [u].[Nickname], [u].[SquadId], [u].[AssignedCityName], [u].[CityOfBirthName], [u].[FullName], [u].[HasSoulPatch], [u].[LeaderNickname], [u].[LeaderSquadId], [u].[Rank], [u].[Discriminator] FROM ( @@ -13707,31 +13707,31 @@ FROM [Officers] AS [o] WHERE CASE WHEN [u].[Rank] IN ( SELECT [r].[value] - FROM OPENJSON(@__ranks_1) WITH ([value] int '$') AS [r] - ) THEN @__key_2 - ELSE @__key_2 + FROM OPENJSON(@__ranks_0) WITH ([value] int '$') AS [r] + ) THEN @__key_1 + ELSE @__key_1 END IN ( SELECT [k].[value] - FROM OPENJSON(@__keys_0) WITH ([value] uniqueidentifier '$') AS [k] + FROM OPENJSON(@__keys_2) WITH ([value] uniqueidentifier '$') AS [k] ) """, // """ -@__ammoTypes_1='[1]' (Size = 4000) -@__key_2='5f221fb9-66f4-442a-92c9-d97ed5989cc7' -@__keys_0='["0a47bcb7-a1cb-4345-8944-c58f82d6aac7","5f221fb9-66f4-442a-92c9-d97ed5989cc7"]' (Size = 4000) +@__ammoTypes_0='[1]' (Size = 4000) +@__key_1='5f221fb9-66f4-442a-92c9-d97ed5989cc7' +@__keys_2='["0a47bcb7-a1cb-4345-8944-c58f82d6aac7","5f221fb9-66f4-442a-92c9-d97ed5989cc7"]' (Size = 4000) SELECT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] FROM [Weapons] AS [w] WHERE CASE WHEN [w].[AmmunitionType] IN ( SELECT [a].[value] - FROM OPENJSON(@__ammoTypes_1) WITH ([value] int '$') AS [a] - ) THEN @__key_2 - ELSE @__key_2 + FROM OPENJSON(@__ammoTypes_0) WITH ([value] int '$') AS [a] + ) THEN @__key_1 + ELSE @__key_1 END IN ( SELECT [k].[value] - FROM OPENJSON(@__keys_0) WITH ([value] uniqueidentifier '$') AS [k] + FROM OPENJSON(@__keys_2) WITH ([value] uniqueidentifier '$') AS [k] ) """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs index 348554a72a3..9c15dc1582b 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TPTGearsOfWarQuerySqlServerTest.cs @@ -11696,9 +11696,9 @@ public override async Task Nested_contains_with_enum(bool async) AssertSql( """ -@__ranks_1='[1]' (Size = 4000) -@__key_2='5f221fb9-66f4-442a-92c9-d97ed5989cc7' -@__keys_0='["0a47bcb7-a1cb-4345-8944-c58f82d6aac7","5f221fb9-66f4-442a-92c9-d97ed5989cc7"]' (Size = 4000) +@__ranks_0='[1]' (Size = 4000) +@__key_1='5f221fb9-66f4-442a-92c9-d97ed5989cc7' +@__keys_2='["0a47bcb7-a1cb-4345-8944-c58f82d6aac7","5f221fb9-66f4-442a-92c9-d97ed5989cc7"]' (Size = 4000) SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[Rank], CASE WHEN [o].[Nickname] IS NOT NULL THEN N'Officer' @@ -11708,31 +11708,31 @@ FROM [Gears] AS [g] WHERE CASE WHEN [g].[Rank] IN ( SELECT [r].[value] - FROM OPENJSON(@__ranks_1) WITH ([value] int '$') AS [r] - ) THEN @__key_2 - ELSE @__key_2 + FROM OPENJSON(@__ranks_0) WITH ([value] int '$') AS [r] + ) THEN @__key_1 + ELSE @__key_1 END IN ( SELECT [k].[value] - FROM OPENJSON(@__keys_0) WITH ([value] uniqueidentifier '$') AS [k] + FROM OPENJSON(@__keys_2) WITH ([value] uniqueidentifier '$') AS [k] ) """, // """ -@__ammoTypes_1='[1]' (Size = 4000) -@__key_2='5f221fb9-66f4-442a-92c9-d97ed5989cc7' -@__keys_0='["0a47bcb7-a1cb-4345-8944-c58f82d6aac7","5f221fb9-66f4-442a-92c9-d97ed5989cc7"]' (Size = 4000) +@__ammoTypes_0='[1]' (Size = 4000) +@__key_1='5f221fb9-66f4-442a-92c9-d97ed5989cc7' +@__keys_2='["0a47bcb7-a1cb-4345-8944-c58f82d6aac7","5f221fb9-66f4-442a-92c9-d97ed5989cc7"]' (Size = 4000) SELECT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[SynergyWithId] FROM [Weapons] AS [w] WHERE CASE WHEN [w].[AmmunitionType] IN ( SELECT [a].[value] - FROM OPENJSON(@__ammoTypes_1) WITH ([value] int '$') AS [a] - ) THEN @__key_2 - ELSE @__key_2 + FROM OPENJSON(@__ammoTypes_0) WITH ([value] int '$') AS [a] + ) THEN @__key_1 + ELSE @__key_1 END IN ( SELECT [k].[value] - FROM OPENJSON(@__keys_0) WITH ([value] uniqueidentifier '$') AS [k] + FROM OPENJSON(@__keys_2) WITH ([value] uniqueidentifier '$') AS [k] ) """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs index 00d0d6aeb89..f80589b4848 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/TemporalGearsOfWarQuerySqlServerTest.cs @@ -10265,40 +10265,40 @@ public override async Task Nested_contains_with_enum(bool async) AssertSql( """ -@__ranks_1='[1]' (Size = 4000) -@__key_2='5f221fb9-66f4-442a-92c9-d97ed5989cc7' -@__keys_0='["0a47bcb7-a1cb-4345-8944-c58f82d6aac7","5f221fb9-66f4-442a-92c9-d97ed5989cc7"]' (Size = 4000) +@__ranks_0='[1]' (Size = 4000) +@__key_1='5f221fb9-66f4-442a-92c9-d97ed5989cc7' +@__keys_2='["0a47bcb7-a1cb-4345-8944-c58f82d6aac7","5f221fb9-66f4-442a-92c9-d97ed5989cc7"]' (Size = 4000) SELECT [g].[Nickname], [g].[SquadId], [g].[AssignedCityName], [g].[CityOfBirthName], [g].[Discriminator], [g].[FullName], [g].[HasSoulPatch], [g].[LeaderNickname], [g].[LeaderSquadId], [g].[PeriodEnd], [g].[PeriodStart], [g].[Rank] FROM [Gears] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [g] WHERE CASE WHEN [g].[Rank] IN ( SELECT [r].[value] - FROM OPENJSON(@__ranks_1) WITH ([value] int '$') AS [r] - ) THEN @__key_2 - ELSE @__key_2 + FROM OPENJSON(@__ranks_0) WITH ([value] int '$') AS [r] + ) THEN @__key_1 + ELSE @__key_1 END IN ( SELECT [k].[value] - FROM OPENJSON(@__keys_0) WITH ([value] uniqueidentifier '$') AS [k] + FROM OPENJSON(@__keys_2) WITH ([value] uniqueidentifier '$') AS [k] ) """, // """ -@__ammoTypes_1='[1]' (Size = 4000) -@__key_2='5f221fb9-66f4-442a-92c9-d97ed5989cc7' -@__keys_0='["0a47bcb7-a1cb-4345-8944-c58f82d6aac7","5f221fb9-66f4-442a-92c9-d97ed5989cc7"]' (Size = 4000) +@__ammoTypes_0='[1]' (Size = 4000) +@__key_1='5f221fb9-66f4-442a-92c9-d97ed5989cc7' +@__keys_2='["0a47bcb7-a1cb-4345-8944-c58f82d6aac7","5f221fb9-66f4-442a-92c9-d97ed5989cc7"]' (Size = 4000) SELECT [w].[Id], [w].[AmmunitionType], [w].[IsAutomatic], [w].[Name], [w].[OwnerFullName], [w].[PeriodEnd], [w].[PeriodStart], [w].[SynergyWithId] FROM [Weapons] FOR SYSTEM_TIME AS OF '2010-01-01T00:00:00.0000000' AS [w] WHERE CASE WHEN [w].[AmmunitionType] IN ( SELECT [a].[value] - FROM OPENJSON(@__ammoTypes_1) WITH ([value] int '$') AS [a] - ) THEN @__key_2 - ELSE @__key_2 + FROM OPENJSON(@__ammoTypes_0) WITH ([value] int '$') AS [a] + ) THEN @__key_1 + ELSE @__key_1 END IN ( SELECT [k].[value] - FROM OPENJSON(@__keys_0) WITH ([value] uniqueidentifier '$') AS [k] + FROM OPENJSON(@__keys_2) WITH ([value] uniqueidentifier '$') AS [k] ) """); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/UdfDbFunctionSqlServerTests.cs b/test/EFCore.SqlServer.FunctionalTests/Query/UdfDbFunctionSqlServerTests.cs index 01b6599f783..6086f0132d3 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/UdfDbFunctionSqlServerTests.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/UdfDbFunctionSqlServerTests.cs @@ -206,12 +206,12 @@ public override void Scalar_Function_Let_Nested_Static() AssertSql( """ -@__starCount_0='3' -@__customerId_1='1' +@__starCount_1='3' +@__customerId_0='1' -SELECT TOP(2) [c].[LastName], [dbo].[StarValue](@__starCount_0, [dbo].[CustomerOrderCount](@__customerId_1)) AS [OrderCount] +SELECT TOP(2) [c].[LastName], [dbo].[StarValue](@__starCount_1, [dbo].[CustomerOrderCount](@__customerId_0)) AS [OrderCount] FROM [Customers] AS [c] -WHERE [c].[Id] = @__customerId_1 +WHERE [c].[Id] = @__customerId_0 """); } @@ -546,12 +546,12 @@ public override void Scalar_Function_Let_Nested_Instance() AssertSql( """ -@__starCount_1='3' -@__customerId_2='1' +@__starCount_2='3' +@__customerId_1='1' -SELECT TOP(2) [c].[LastName], [dbo].[StarValue](@__starCount_1, [dbo].[CustomerOrderCount](@__customerId_2)) AS [OrderCount] +SELECT TOP(2) [c].[LastName], [dbo].[StarValue](@__starCount_2, [dbo].[CustomerOrderCount](@__customerId_1)) AS [OrderCount] FROM [Customers] AS [c] -WHERE [c].[Id] = @__customerId_2 +WHERE [c].[Id] = @__customerId_1 """); }