Skip to content

Commit

Permalink
Implement EF.Constant (dotnet#32412)
Browse files Browse the repository at this point in the history
Closes dotnet#31552

(cherry picked from commit 484a3ed)
  • Loading branch information
roji committed Nov 30, 2023
1 parent 141740f commit c55c350
Show file tree
Hide file tree
Showing 11 changed files with 255 additions and 1 deletion.
11 changes: 11 additions & 0 deletions src/EFCore/EF.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@ public static TProperty Property<TProperty>(
[NotParameterized] string propertyName)
=> throw new InvalidOperationException(CoreStrings.PropertyMethodInvoked);

/// <summary>
/// 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.
/// </summary>
/// <remarks>Note that this is a static method accessed through the top-level <see cref="EF" /> static type.</remarks>
/// <typeparam name="T">The type of the expression to be integrated as a constant into the query.</typeparam>
/// <param name="argument">The expression to be integrated as a constant into the query.</param>
/// <returns>The same value for further use in the query.</returns>
public static T Constant<T>(T argument)
=> throw new InvalidOperationException(CoreStrings.EFConstantInvoked);

/// <summary>
/// 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 <see cref="NotSupportedException" />.
Expand Down
12 changes: 12 additions & 0 deletions src/EFCore/Properties/CoreStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/EFCore/Properties/CoreStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,12 @@
<data name="DuplicateTrigger" xml:space="preserve">
<value>The trigger '{trigger}' cannot be added to the entity type '{entityType}' because another trigger with the same name already exists on entity type '{conflictingEntityType}'.</value>
</data>
<data name="EFConstantInvoked" xml:space="preserve">
<value>The EF.Constant&lt;T&gt; method may only be used within Entity Framework LINQ queries.</value>
</data>
<data name="EFConstantWithNonEvaluableArgument" xml:space="preserve">
<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>
</data>
<data name="EmptyComplexType" xml:space="preserve">
<value>Complex type '{complexType}' has no properties defines. Configure at least one property or don't include this type in the model.</value>
</data>
Expand Down
40 changes: 40 additions & 0 deletions src/EFCore/Query/Internal/ParameterExtractingExpressionVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ public class ParameterExtractingExpressionVisitor : ExpressionVisitor
private IDictionary<Expression, bool> _evaluatableExpressions;
private IQueryProvider? _currentQueryProvider;

private static readonly bool UseOldBehavior31552 =
AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue31552", out var enabled31552) && enabled31552;

/// <summary>
/// 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
Expand Down Expand Up @@ -170,6 +173,33 @@ protected override Expression VisitConditional(ConditionalExpression conditional
Visit(conditionalExpression.IfFalse));
}

/// <summary>
/// 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.
/// </summary>
protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
{
if (!UseOldBehavior31552
&& methodCallExpression.Method.DeclaringType == typeof(EF)
&& methodCallExpression.Method.Name == nameof(EF.Constant))
{
// 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 _))
{
throw new InvalidOperationException(CoreStrings.EFConstantWithNonEvaluableArgument);
}

return Evaluate(operand, generateParameter: false);
}

return base.VisitMethodCall(methodCallExpression);
}

/// <summary>
/// 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
Expand Down Expand Up @@ -655,6 +685,16 @@ private static bool IsEvaluatableNodeType(Expression expression, out bool prefer
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 !UseOldBehavior31552 && expression is MethodCallExpression { Method: var method }
&& method.DeclaringType == typeof(EF)
&& method.Name == nameof(EF.Constant):
preferNoEvaluation = true;
return false;

default:
preferNoEvaluation = false;
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2907,6 +2907,49 @@ FROM root c
""");
}

public override async Task EF_Constant(bool async)
{
await base.EF_Constant(async);

AssertSql(
"""
SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = "ALFKI"))
""");
}

public override async Task EF_Constant_with_subtree(bool async)
{
await base.EF_Constant_with_subtree(async);

AssertSql(
"""
SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = "ALFKI"))
""");
}

public override async Task EF_Constant_does_not_parameterized_as_part_of_bigger_subtree(bool async)
{
await base.EF_Constant_does_not_parameterized_as_part_of_bigger_subtree(async);

AssertSql(
"""
SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Customer") AND (c["CustomerID"] = ("ALF" || "KI")))
""");
}

public override async Task EF_Constant_with_non_evaluatable_argument_throws(bool async)
{
await base.EF_Constant_with_non_evaluatable_argument_throws(async);

AssertSql();
}

private void AssertSql(params string[] expected)
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@

using Microsoft.EntityFrameworkCore.TestModels.Northwind;

namespace Microsoft.EntityFrameworkCore.Query;

// ReSharper disable ConvertToConstant.Local
// ReSharper disable RedundantBoolCompare
// ReSharper disable InconsistentNaming
namespace Microsoft.EntityFrameworkCore.Query;

public abstract class NorthwindWhereQueryTestBase<TFixture> : QueryTestBase<TFixture>
where TFixture : NorthwindQueryFixtureBase<NoopModelCustomizer>, new()
Expand Down Expand Up @@ -2342,4 +2344,53 @@ public virtual Task Case_block_simplification_works_correctly(bool async)
async,
ss => ss.Set<Customer>().Where(c => (c.Region == null ? "OR" : c.Region) == "OR"));
#pragma warning restore IDE0029 // Use coalesce expression

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task EF_Constant(bool async)
{
var id = "ALFKI";

return AssertQuery(
async,
ss => ss.Set<Customer>().Where(c => c.CustomerID == EF.Constant(id)),
ss => ss.Set<Customer>().Where(c => c.CustomerID == "ALFKI"));
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task EF_Constant_with_subtree(bool async)
{
var i = "ALF";
var j = "KI";

return AssertQuery(
async,
ss => ss.Set<Customer>().Where(c => c.CustomerID == EF.Constant(i + j)),
ss => ss.Set<Customer>().Where(c => c.CustomerID == "ALFKI"));
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task EF_Constant_does_not_parameterized_as_part_of_bigger_subtree(bool async)
{
var id = "ALF";

return AssertQuery(
async,
ss => ss.Set<Customer>().Where(c => c.CustomerID == EF.Constant(id) + "KI"),
ss => ss.Set<Customer>().Where(c => c.CustomerID == "ALF" + "KI"));
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task EF_Constant_with_non_evaluatable_argument_throws(bool async)
{
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
() => AssertQuery(
async,
ss => ss.Set<Customer>().Where(c => c.CustomerID == EF.Constant(c.CustomerID))));

Assert.Equal(CoreStrings.EFConstantWithNonEvaluableArgument, exception.Message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,18 @@ public virtual Task Inline_collection_Contains_with_three_values(bool async)
async,
ss => ss.Set<PrimitiveCollectionsEntity>().Where(c => new[] { 2, 999, 1000 }.Contains(c.Id)));

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Inline_collection_Contains_with_EF_Constant(bool async)
{
var ids = new[] { 2, 999, 1000 };

return AssertQuery(
async,
ss => ss.Set<PrimitiveCollectionsEntity>().Where(c => EF.Constant(ids).Contains(c.Id)),
ss => ss.Set<PrimitiveCollectionsEntity>().Where(c => new[] { 2, 99, 1000 }.Contains(c.Id)));
}

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Inline_collection_Contains_with_all_parameters(bool async)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3243,6 +3243,49 @@ public override async Task Where_client_deep_inside_predicate_and_server_top_lev
AssertSql();
}

public override async Task EF_Constant(bool async)
{
await base.EF_Constant(async);

AssertSql(
"""
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'
""");
}

public override async Task EF_Constant_with_subtree(bool async)
{
await base.EF_Constant_with_subtree(async);

AssertSql(
"""
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'
""");
}

public override async Task EF_Constant_does_not_parameterized_as_part_of_bigger_subtree(bool async)
{
await base.EF_Constant_does_not_parameterized_as_part_of_bigger_subtree(async);

AssertSql(
"""
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'ALF' + N'KI'
""");
}

public override async Task EF_Constant_with_non_evaluatable_argument_throws(bool async)
{
await base.EF_Constant_with_non_evaluatable_argument_throws(async);

AssertSql();
}

private void AssertSql(params string[] expected)
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,18 @@ WHERE [p].[Id] IN (2, 999, 1000)
""");
}

public override async Task Inline_collection_Contains_with_EF_Constant(bool async)
{
await base.Inline_collection_Contains_with_EF_Constant(async);

AssertSql(
"""
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 [p].[Id] IN (2, 999, 1000)
""");
}

public override async Task Inline_collection_Contains_with_all_parameters(bool async)
{
await base.Inline_collection_Contains_with_all_parameters(async);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,18 @@ WHERE [p].[Id] IN (2, 999, 1000)
""");
}

public override async Task Inline_collection_Contains_with_EF_Constant(bool async)
{
await base.Inline_collection_Contains_with_EF_Constant(async);

AssertSql(
"""
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 [p].[Id] IN (2, 999, 1000)
""");
}

public override async Task Inline_collection_Contains_with_all_parameters(bool async)
{
await base.Inline_collection_Contains_with_all_parameters(async);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,18 @@ public override async Task Inline_collection_Contains_with_three_values(bool asy
""");
}

public override async Task Inline_collection_Contains_with_EF_Constant(bool async)
{
await base.Inline_collection_Contains_with_EF_Constant(async);

AssertSql(
"""
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 "p"."Id" IN (2, 999, 1000)
""");
}

public override async Task Inline_collection_Contains_with_all_parameters(bool async)
{
await base.Inline_collection_Contains_with_all_parameters(async);
Expand Down

0 comments on commit c55c350

Please sign in to comment.