From 7ed714a7d0f19e9a10429af51490acd73ab9731d Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Tue, 28 Nov 2023 00:53:51 +0100 Subject: [PATCH] Implement EF.Parameter Closes #28151 --- src/EFCore/EF.cs | 21 +++++--- src/EFCore/Properties/CoreStrings.Designer.cs | 12 +++++ src/EFCore/Properties/CoreStrings.resx | 3 ++ .../ParameterExtractingExpressionVisitor.cs | 37 ++++++++++---- .../Query/NorthwindWhereQueryCosmosTest.cs | 49 +++++++++++++++++++ .../Query/NorthwindWhereQueryTestBase.cs | 47 ++++++++++++++++++ .../Query/NorthwindWhereQuerySqlServerTest.cs | 49 +++++++++++++++++++ 7 files changed, 200 insertions(+), 18 deletions(-) diff --git a/src/EFCore/EF.cs b/src/EFCore/EF.cs index 21824e50c6b..f7a5c0b5713 100644 --- a/src/EFCore/EF.cs +++ b/src/EFCore/EF.cs @@ -67,20 +67,25 @@ public static TProperty Property( /// Within the context of an EF LINQ query, forces its argument to be inserted into the query as a constant expression. This can be /// used to e.g. integrate a value as a constant inside an EF query, instead of as a parameter, for query performance reasons. /// - /// - /// - /// Note that this is a static method accessed through the top-level static type. - /// - /// - /// See Using EF.Property in EF Core queries for more information and examples. - /// - /// + /// Note that this is a static method accessed through the top-level static type. /// The type of the expression to be integrated as a constant into the query. /// The expression to be integrated as a constant into the query. /// The same value for further use in the query. public static T Constant(T argument) => throw new InvalidOperationException(CoreStrings.EFConstantInvoked); + /// + /// Within the context of an EF LINQ query, forces its argument to be inserted into the query as a parameter expression. This can be + /// used to e.g. make sure a constant value is parameterized instead of integrated as a constant into the query, which can be useful + /// in dynamic query construction scenarios. + /// + /// Note that this is a static method accessed through the top-level static type. + /// The type of the expression to be integrated as a parameter into the query. + /// The expression to be integrated as a parameter into the query. + /// The same value for further use in the query. + public static T Parameter(T argument) + => throw new InvalidOperationException(CoreStrings.EFParameterInvoked); + /// /// Provides CLR methods that get translated to database functions when used in LINQ to Entities queries. /// Calling these methods in other contexts (e.g. LINQ to Objects) will throw a . diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 957537deb5f..458ee21dd2b 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -976,6 +976,18 @@ public static string EFConstantInvoked public static string EFConstantWithNonEvaluableArgument => GetString("EFConstantWithNonEvaluableArgument"); + /// + /// The EF.Parameter<T> method may only be used within Entity Framework LINQ queries. + /// + 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 EFParameterWithNonEvaluableArgument + => GetString("EFParameterWithNonEvaluableArgument"); + /// /// Complex type '{complexType}' has no properties defines. Configure at least one property or don't include this type in the model. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 0db53ec29e7..4fef33b7946 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -480,6 +480,9 @@ 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. + Complex type '{complexType}' has no properties defines. Configure at least one property or don't include this type in the model. diff --git a/src/EFCore/Query/Internal/ParameterExtractingExpressionVisitor.cs b/src/EFCore/Query/Internal/ParameterExtractingExpressionVisitor.cs index afc8cd415b6..d3057838ead 100644 --- a/src/EFCore/Query/Internal/ParameterExtractingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/ParameterExtractingExpressionVisitor.cs @@ -178,18 +178,35 @@ protected override Expression VisitConditional(ConditionalExpression conditional /// protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression) { - if (methodCallExpression.Method.DeclaringType == typeof(EF) && methodCallExpression.Method.Name == nameof(EF.Constant)) + // 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)) { - // If this is a call to EF.Constant(), then examine its operand. If the operand isn't evaluatable (i.e. contains a reference - // to a database table), throw immediately. - // Otherwise, evaluate the operand as a constant and return that. - var operand = methodCallExpression.Arguments[0]; - if (!_evaluatableExpressions.TryGetValue(operand, out _)) + switch (methodCallExpression.Method.Name) { - throw new InvalidOperationException(CoreStrings.EFConstantWithNonEvaluableArgument); - } + case nameof(EF.Constant): + { + var operand = methodCallExpression.Arguments[0]; + if (!_evaluatableExpressions.TryGetValue(operand, out _)) + { + throw new InvalidOperationException(CoreStrings.EFConstantWithNonEvaluableArgument); + } + + return Evaluate(operand, generateParameter: false); + } - 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); @@ -686,7 +703,7 @@ private static bool IsEvaluatableNodeType(Expression expression, out bool prefer case ExpressionType.Call when expression is MethodCallExpression { Method: var method } && method.DeclaringType == typeof(EF) - && method.Name == nameof(EF.Constant): + && method.Name is nameof(EF.Constant) or nameof(EF.Parameter): preferNoEvaluation = true; return false; diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs index 2d1cf3837f1..fe55aa92743 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs @@ -2946,6 +2946,55 @@ public override async Task EF_Constant_with_non_evaluatable_argument_throws(bool AssertSql(); } + public override async Task EF_Parameter(bool async) + { + await base.EF_Parameter(async); + + AssertSql( + """ +@__p_0='ALFKI' + +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = @__p_0)) +"""); + } + + public override async Task EF_Parameter_with_subtree(bool async) + { + await base.EF_Parameter_with_subtree(async); + + AssertSql( + """ +@__p_0='ALFKI' + +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = @__p_0)) +"""); + } + + public override async Task EF_Parameter_does_not_parameterized_as_part_of_bigger_subtree(bool async) + { + await base.EF_Parameter_does_not_parameterized_as_part_of_bigger_subtree(async); + + AssertSql( + """ +@__id_0='ALF' + +SELECT c +FROM root c +WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = (@__id_0 || "KI"))) +"""); + } + + public override async Task EF_Parameter_with_non_evaluatable_argument_throws(bool async) + { + await base.EF_Parameter_with_non_evaluatable_argument_throws(async); + + AssertSql(); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs index fd68f8a0375..9c094ec6a0a 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs @@ -2393,4 +2393,51 @@ public virtual async Task EF_Constant_with_non_evaluatable_argument_throws(bool Assert.Equal(CoreStrings.EFConstantWithNonEvaluableArgument, exception.Message); } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task EF_Parameter(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => c.CustomerID == EF.Parameter("ALFKI")), + ss => ss.Set().Where(c => c.CustomerID == "ALFKI")); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task EF_Parameter_with_subtree(bool async) + { + // This is a somewhat silly scenario: a subtree would get parameterized anyway, with or without EF.Parameter(). + // But including for completeness. + var i = "ALF"; + var j = "KI"; + + return AssertQuery( + async, + ss => ss.Set().Where(c => c.CustomerID == EF.Parameter(i + j)), + ss => ss.Set().Where(c => c.CustomerID == "ALFKI")); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task EF_Parameter_does_not_parameterized_as_part_of_bigger_subtree(bool async) + { + var id = "ALF"; + + return AssertQuery( + async, + ss => ss.Set().Where(c => c.CustomerID == EF.Parameter(id) + "KI"), + ss => ss.Set().Where(c => c.CustomerID == "ALF" + "KI")); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task EF_Parameter_with_non_evaluatable_argument_throws(bool async) + { + var exception = await Assert.ThrowsAsync( + () => AssertQuery( + async, + ss => ss.Set().Where(c => c.CustomerID == EF.Parameter(c.CustomerID)))); + + Assert.Equal(CoreStrings.EFConstantWithNonEvaluableArgument, exception.Message); + } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs index 156c8bc3c39..387cdc5b1bf 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs @@ -3286,6 +3286,55 @@ public override async Task EF_Constant_with_non_evaluatable_argument_throws(bool AssertSql(); } + public override async Task EF_Parameter(bool async) + { + await base.EF_Parameter(async); + + AssertSql( + """ +@__p_0='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] = @__p_0 +"""); + } + + public override async Task EF_Parameter_with_subtree(bool async) + { + await base.EF_Parameter_with_subtree(async); + + AssertSql( + """ +@__p_0='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] = @__p_0 +"""); + } + + public override async Task EF_Parameter_does_not_parameterized_as_part_of_bigger_subtree(bool async) + { + await base.EF_Parameter_does_not_parameterized_as_part_of_bigger_subtree(async); + + AssertSql( + """ +@__id_0='ALF' (Size = 5) + +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] = @__id_0 + N'KI' +"""); + } + + public override async Task EF_Parameter_with_non_evaluatable_argument_throws(bool async) + { + await base.EF_Parameter_with_non_evaluatable_argument_throws(async); + + AssertSql(); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected);