Skip to content

Commit 7ed714a

Browse files
committed
Implement EF.Parameter
Closes #28151
1 parent 484a3ed commit 7ed714a

File tree

7 files changed

+200
-18
lines changed

7 files changed

+200
-18
lines changed

src/EFCore/EF.cs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,20 +67,25 @@ public static TProperty Property<TProperty>(
6767
/// Within the context of an EF LINQ query, forces its argument to be inserted into the query as a constant expression. This can be
6868
/// used to e.g. integrate a value as a constant inside an EF query, instead of as a parameter, for query performance reasons.
6969
/// </summary>
70-
/// <remarks>
71-
/// <para>
72-
/// Note that this is a static method accessed through the top-level <see cref="EF" /> static type.
73-
/// </para>
74-
/// <para>
75-
/// See <see href="https://aka.ms/efcore-docs-efproperty">Using EF.Property in EF Core queries</see> for more information and examples.
76-
/// </para>
77-
/// </remarks>
70+
/// <remarks>Note that this is a static method accessed through the top-level <see cref="EF" /> static type.</remarks>
7871
/// <typeparam name="T">The type of the expression to be integrated as a constant into the query.</typeparam>
7972
/// <param name="argument">The expression to be integrated as a constant into the query.</param>
8073
/// <returns>The same value for further use in the query.</returns>
8174
public static T Constant<T>(T argument)
8275
=> throw new InvalidOperationException(CoreStrings.EFConstantInvoked);
8376

77+
/// <summary>
78+
/// Within the context of an EF LINQ query, forces its argument to be inserted into the query as a parameter expression. This can be
79+
/// used to e.g. make sure a constant value is parameterized instead of integrated as a constant into the query, which can be useful
80+
/// in dynamic query construction scenarios.
81+
/// </summary>
82+
/// <remarks>Note that this is a static method accessed through the top-level <see cref="EF" /> static type.</remarks>
83+
/// <typeparam name="T">The type of the expression to be integrated as a parameter into the query.</typeparam>
84+
/// <param name="argument">The expression to be integrated as a parameter into the query.</param>
85+
/// <returns>The same value for further use in the query.</returns>
86+
public static T Parameter<T>(T argument)
87+
=> throw new InvalidOperationException(CoreStrings.EFParameterInvoked);
88+
8489
/// <summary>
8590
/// Provides CLR methods that get translated to database functions when used in LINQ to Entities queries.
8691
/// Calling these methods in other contexts (e.g. LINQ to Objects) will throw a <see cref="NotSupportedException" />.

src/EFCore/Properties/CoreStrings.Designer.cs

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EFCore/Properties/CoreStrings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,9 @@
480480
<data name="EFConstantWithNonEvaluableArgument" xml:space="preserve">
481481
<value>The EF.Constant&lt;T&gt; method may only be used with an argument that can be evaluated client-side and does not contain any reference to database-side entities.</value>
482482
</data>
483+
<data name="EFParameterInvoked" xml:space="preserve">
484+
<value>The EF.Parameter&lt;T&gt; method may only be used within Entity Framework LINQ queries.</value>
485+
</data>
483486
<data name="EmptyComplexType" xml:space="preserve">
484487
<value>Complex type '{complexType}' has no properties defines. Configure at least one property or don't include this type in the model.</value>
485488
</data>

src/EFCore/Query/Internal/ParameterExtractingExpressionVisitor.cs

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -178,18 +178,35 @@ protected override Expression VisitConditional(ConditionalExpression conditional
178178
/// </summary>
179179
protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
180180
{
181-
if (methodCallExpression.Method.DeclaringType == typeof(EF) && methodCallExpression.Method.Name == nameof(EF.Constant))
181+
// 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
182+
// reference to a database table), throw immediately. Otherwise, evaluate the operand (either as a constant or as a parameter) and
183+
// return that.
184+
if (methodCallExpression.Method.DeclaringType == typeof(EF))
182185
{
183-
// If this is a call to EF.Constant(), then examine its operand. If the operand isn't evaluatable (i.e. contains a reference
184-
// to a database table), throw immediately.
185-
// Otherwise, evaluate the operand as a constant and return that.
186-
var operand = methodCallExpression.Arguments[0];
187-
if (!_evaluatableExpressions.TryGetValue(operand, out _))
186+
switch (methodCallExpression.Method.Name)
188187
{
189-
throw new InvalidOperationException(CoreStrings.EFConstantWithNonEvaluableArgument);
190-
}
188+
case nameof(EF.Constant):
189+
{
190+
var operand = methodCallExpression.Arguments[0];
191+
if (!_evaluatableExpressions.TryGetValue(operand, out _))
192+
{
193+
throw new InvalidOperationException(CoreStrings.EFConstantWithNonEvaluableArgument);
194+
}
195+
196+
return Evaluate(operand, generateParameter: false);
197+
}
191198

192-
return Evaluate(operand, generateParameter: false);
199+
case nameof(EF.Parameter):
200+
{
201+
var operand = methodCallExpression.Arguments[0];
202+
if (!_evaluatableExpressions.TryGetValue(operand, out _))
203+
{
204+
throw new InvalidOperationException(CoreStrings.EFConstantWithNonEvaluableArgument);
205+
}
206+
207+
return Evaluate(operand, generateParameter: true);
208+
}
209+
}
193210
}
194211

195212
return base.VisitMethodCall(methodCallExpression);
@@ -686,7 +703,7 @@ private static bool IsEvaluatableNodeType(Expression expression, out bool prefer
686703
case ExpressionType.Call
687704
when expression is MethodCallExpression { Method: var method }
688705
&& method.DeclaringType == typeof(EF)
689-
&& method.Name == nameof(EF.Constant):
706+
&& method.Name is nameof(EF.Constant) or nameof(EF.Parameter):
690707
preferNoEvaluation = true;
691708
return false;
692709

test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2946,6 +2946,55 @@ public override async Task EF_Constant_with_non_evaluatable_argument_throws(bool
29462946
AssertSql();
29472947
}
29482948

2949+
public override async Task EF_Parameter(bool async)
2950+
{
2951+
await base.EF_Parameter(async);
2952+
2953+
AssertSql(
2954+
"""
2955+
@__p_0='ALFKI'
2956+
2957+
SELECT c
2958+
FROM root c
2959+
WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = @__p_0))
2960+
""");
2961+
}
2962+
2963+
public override async Task EF_Parameter_with_subtree(bool async)
2964+
{
2965+
await base.EF_Parameter_with_subtree(async);
2966+
2967+
AssertSql(
2968+
"""
2969+
@__p_0='ALFKI'
2970+
2971+
SELECT c
2972+
FROM root c
2973+
WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = @__p_0))
2974+
""");
2975+
}
2976+
2977+
public override async Task EF_Parameter_does_not_parameterized_as_part_of_bigger_subtree(bool async)
2978+
{
2979+
await base.EF_Parameter_does_not_parameterized_as_part_of_bigger_subtree(async);
2980+
2981+
AssertSql(
2982+
"""
2983+
@__id_0='ALF'
2984+
2985+
SELECT c
2986+
FROM root c
2987+
WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = (@__id_0 || "KI")))
2988+
""");
2989+
}
2990+
2991+
public override async Task EF_Parameter_with_non_evaluatable_argument_throws(bool async)
2992+
{
2993+
await base.EF_Parameter_with_non_evaluatable_argument_throws(async);
2994+
2995+
AssertSql();
2996+
}
2997+
29492998
private void AssertSql(params string[] expected)
29502999
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
29513000

test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2393,4 +2393,51 @@ public virtual async Task EF_Constant_with_non_evaluatable_argument_throws(bool
23932393

23942394
Assert.Equal(CoreStrings.EFConstantWithNonEvaluableArgument, exception.Message);
23952395
}
2396+
2397+
[ConditionalTheory]
2398+
[MemberData(nameof(IsAsyncData))]
2399+
public virtual Task EF_Parameter(bool async)
2400+
=> AssertQuery(
2401+
async,
2402+
ss => ss.Set<Customer>().Where(c => c.CustomerID == EF.Parameter("ALFKI")),
2403+
ss => ss.Set<Customer>().Where(c => c.CustomerID == "ALFKI"));
2404+
2405+
[ConditionalTheory]
2406+
[MemberData(nameof(IsAsyncData))]
2407+
public virtual Task EF_Parameter_with_subtree(bool async)
2408+
{
2409+
// This is a somewhat silly scenario: a subtree would get parameterized anyway, with or without EF.Parameter().
2410+
// But including for completeness.
2411+
var i = "ALF";
2412+
var j = "KI";
2413+
2414+
return AssertQuery(
2415+
async,
2416+
ss => ss.Set<Customer>().Where(c => c.CustomerID == EF.Parameter(i + j)),
2417+
ss => ss.Set<Customer>().Where(c => c.CustomerID == "ALFKI"));
2418+
}
2419+
2420+
[ConditionalTheory]
2421+
[MemberData(nameof(IsAsyncData))]
2422+
public virtual Task EF_Parameter_does_not_parameterized_as_part_of_bigger_subtree(bool async)
2423+
{
2424+
var id = "ALF";
2425+
2426+
return AssertQuery(
2427+
async,
2428+
ss => ss.Set<Customer>().Where(c => c.CustomerID == EF.Parameter(id) + "KI"),
2429+
ss => ss.Set<Customer>().Where(c => c.CustomerID == "ALF" + "KI"));
2430+
}
2431+
2432+
[ConditionalTheory]
2433+
[MemberData(nameof(IsAsyncData))]
2434+
public virtual async Task EF_Parameter_with_non_evaluatable_argument_throws(bool async)
2435+
{
2436+
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
2437+
() => AssertQuery(
2438+
async,
2439+
ss => ss.Set<Customer>().Where(c => c.CustomerID == EF.Parameter(c.CustomerID))));
2440+
2441+
Assert.Equal(CoreStrings.EFConstantWithNonEvaluableArgument, exception.Message);
2442+
}
23962443
}

test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3286,6 +3286,55 @@ public override async Task EF_Constant_with_non_evaluatable_argument_throws(bool
32863286
AssertSql();
32873287
}
32883288

3289+
public override async Task EF_Parameter(bool async)
3290+
{
3291+
await base.EF_Parameter(async);
3292+
3293+
AssertSql(
3294+
"""
3295+
@__p_0='ALFKI' (Size = 5) (DbType = StringFixedLength)
3296+
3297+
SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region]
3298+
FROM [Customers] AS [c]
3299+
WHERE [c].[CustomerID] = @__p_0
3300+
""");
3301+
}
3302+
3303+
public override async Task EF_Parameter_with_subtree(bool async)
3304+
{
3305+
await base.EF_Parameter_with_subtree(async);
3306+
3307+
AssertSql(
3308+
"""
3309+
@__p_0='ALFKI' (Size = 5) (DbType = StringFixedLength)
3310+
3311+
SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region]
3312+
FROM [Customers] AS [c]
3313+
WHERE [c].[CustomerID] = @__p_0
3314+
""");
3315+
}
3316+
3317+
public override async Task EF_Parameter_does_not_parameterized_as_part_of_bigger_subtree(bool async)
3318+
{
3319+
await base.EF_Parameter_does_not_parameterized_as_part_of_bigger_subtree(async);
3320+
3321+
AssertSql(
3322+
"""
3323+
@__id_0='ALF' (Size = 5)
3324+
3325+
SELECT [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region]
3326+
FROM [Customers] AS [c]
3327+
WHERE [c].[CustomerID] = @__id_0 + N'KI'
3328+
""");
3329+
}
3330+
3331+
public override async Task EF_Parameter_with_non_evaluatable_argument_throws(bool async)
3332+
{
3333+
await base.EF_Parameter_with_non_evaluatable_argument_throws(async);
3334+
3335+
AssertSql();
3336+
}
3337+
32893338
private void AssertSql(params string[] expected)
32903339
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
32913340

0 commit comments

Comments
 (0)