Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel;
using Microsoft.EntityFrameworkCore.Internal;

namespace Microsoft.EntityFrameworkCore.Infrastructure;

Expand Down Expand Up @@ -159,6 +160,54 @@ public virtual TBuilder ExecutionStrategy(
=> WithOption(
e => (TExtension)e.WithExecutionStrategyFactory(Check.NotNull(getExecutionStrategy, nameof(getExecutionStrategy))));

/// <summary>
/// Configures the context to translate parameterized collections to inline constants.
/// </summary>
/// <remarks>
/// <para>
/// When a LINQ query contains a parameterized collection, by default EF Core parameterizes the entire collection as a single
/// SQL parameter, if possible. For example, on SQL Server, the LINQ query <c>Where(b => ids.Contains(b.Id)</c> is translated to
/// <c>WHERE [b].[Id] IN (SELECT [i].[value] FROM OPENJSON(@__ids_0) ...)</c>. While this helps with query plan caching, it can
/// produce worse query plans for certain query types.
/// </para>
/// <para>
/// <see cref="TranslateParameterizedCollectionsToConstants" /> instructs EF to translate the collection to a set of constants:
/// <c>WHERE [b].[Id] IN (1, 2, 3)</c>. This can produce better query plans for certain query types, but can also lead to query
/// plan bloat.
/// </para>
/// <para>
/// Note that it's possible to cause EF to translate a specific collection in a specific query to constants by wrapping the
/// parameterized collection in <see cref="EF.Constant{T}" />: <c>Where(b => EF.Constant(ids).Contains(b.Id)</c>. This overrides
/// the default.
/// </para>
/// </remarks>
public virtual TBuilder TranslateParameterizedCollectionsToConstants()
=> WithOption(e => (TExtension)e.WithParameterizedCollectionTranslationMode(ParameterizedCollectionTranslationMode.Constantize));

/// <summary>
/// Configures the context to translate parameterized collections to inline constants.
/// </summary>
/// <remarks>
/// <para>
/// When a LINQ query contains a parameterized collection, by default EF Core parameterizes the entire collection as a single
/// SQL parameter, if possible. For example, on SQL Server, the LINQ query <c>Where(b => ids.Contains(b.Id)</c> is translated to
/// <c>WHERE [b].[Id] IN (SELECT [i].[value] FROM OPENJSON(@__ids_0) ...)</c>. While this helps with query plan caching, it can
/// produce worse query plans for certain query types.
/// </para>
/// <para>
/// <see cref="TranslateParameterizedCollectionsToConstants" /> instructs EF to translate the collection to a set of constants:
/// <c>WHERE [b].[Id] IN (1, 2, 3)</c>. This can produce better query plans for certain query types, but can also lead to query
/// plan bloat.
/// </para>
/// <para>
/// Note that it's possible to cause EF to translate a specific collection in a specific query to constants by wrapping the
/// parameterized collection in <see cref="EF.Constant{T}" />: <c>Where(b => EF.Constant(ids).Contains(b.Id)</c>. This overrides
/// the default.
/// </para>
/// </remarks>
public virtual TBuilder TranslateParameterizedCollectionsToParameters()
=> WithOption(e => (TExtension)e.WithParameterizedCollectionTranslationMode(ParameterizedCollectionTranslationMode.Parameterize));

/// <summary>
/// Sets an option by cloning the extension used to store the settings. This ensures the builder
/// does not modify options that are already in use elsewhere.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text;
using Microsoft.EntityFrameworkCore.Internal;

namespace Microsoft.EntityFrameworkCore.Infrastructure;

Expand Down Expand Up @@ -33,10 +34,10 @@ public abstract class RelationalOptionsExtension : IDbContextOptionsExtension
private QuerySplittingBehavior? _querySplittingBehavior;
private string? _migrationsAssembly;
private Assembly? _migrationsAssemblyObject;

private string? _migrationsHistoryTableName;
private string? _migrationsHistoryTableSchema;
private Func<ExecutionStrategyDependencies, IExecutionStrategy>? _executionStrategyFactory;
private ParameterizedCollectionTranslationMode? _parameterizedCollectionTranslationMode;

/// <summary>
/// Creates a new set of options with everything set to default values.
Expand All @@ -63,6 +64,7 @@ protected RelationalOptionsExtension(RelationalOptionsExtension copyFrom)
_migrationsHistoryTableName = copyFrom._migrationsHistoryTableName;
_migrationsHistoryTableSchema = copyFrom._migrationsHistoryTableSchema;
_executionStrategyFactory = copyFrom._executionStrategyFactory;
_parameterizedCollectionTranslationMode = copyFrom._parameterizedCollectionTranslationMode;
}

/// <summary>
Expand Down Expand Up @@ -381,6 +383,26 @@ public virtual RelationalOptionsExtension WithExecutionStrategyFactory(
return clone;
}

/// <summary>
/// Configured translation mode for parameterized collections.
/// </summary>
public virtual ParameterizedCollectionTranslationMode? ParameterizedCollectionTranslationMode
=> _parameterizedCollectionTranslationMode;

/// <summary>
/// Creates a new instance with all options the same as for this instance, but with the given option changed.
/// It is unusual to call this method directly. Instead use <see cref="DbContextOptionsBuilder" />.
/// </summary>
/// <param name="parameterizedCollectionTranslationMode">The option to change.</param>
public virtual RelationalOptionsExtension WithParameterizedCollectionTranslationMode(ParameterizedCollectionTranslationMode parameterizedCollectionTranslationMode)
{
var clone = Clone();

clone._parameterizedCollectionTranslationMode = parameterizedCollectionTranslationMode;

return clone;
}

/// <summary>
/// Finds an existing <see cref="RelationalOptionsExtension" /> registered on the given options
/// or throws if none has been registered. This is typically used to find some relational
Expand Down Expand Up @@ -540,6 +562,11 @@ public override string LogFragment
builder.Append(Extension._migrationsHistoryTableName ?? HistoryRepository.DefaultTableName).Append(' ');
}

if (Extension._parameterizedCollectionTranslationMode != null)
{
builder.Append("ParameterizedCollectionTranslationMode=").Append(Extension._parameterizedCollectionTranslationMode).Append(' ');
}

_logFragment = builder.ToString();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.EntityFrameworkCore.Internal;

/// <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>
public enum ParameterizedCollectionTranslationMode
{
/// <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>
Constantize = 0,

/// <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>
Parameterize,
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public RelationalCommandCache(
IRelationalParameterBasedSqlProcessorFactory relationalParameterBasedSqlProcessorFactory,
Expression queryExpression,
bool useRelationalNulls,
HashSet<string> parametersToConstantize)
IReadOnlySet<string> parametersToConstantize)
{
_memoryCache = memoryCache;
_querySqlGeneratorFactory = querySqlGeneratorFactory;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ public sealed record RelationalParameterBasedSqlProcessorParameters
/// <summary>
/// A collection of parameter names to constantize.
/// </summary>
public HashSet<string> ParametersToConstantize { get; init; }
public IReadOnlySet<string> ParametersToConstantize { get; init; }

/// <summary>
/// Creates a new instance of <see cref="RelationalParameterBasedSqlProcessorParameters" />.
/// </summary>
/// <param name="useRelationalNulls">A value indicating if relational nulls should be used.</param>
/// <param name="parametersToConstantize">A collection of parameter names to constantize.</param>
[EntityFrameworkInternal]
public RelationalParameterBasedSqlProcessorParameters(bool useRelationalNulls, HashSet<string> parametersToConstantize)
public RelationalParameterBasedSqlProcessorParameters(bool useRelationalNulls, IReadOnlySet<string> parametersToConstantize)
{
UseRelationalNulls = useRelationalNulls;
ParametersToConstantize = parametersToConstantize;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Query.Internal;
Expand Down Expand Up @@ -242,7 +243,8 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
&& method.GetGenericMethodDefinition() == QueryableMethods.Contains
&& methodCallExpression.Arguments[0] is ParameterQueryRootExpression parameterSource
&& TranslateExpression(methodCallExpression.Arguments[1]) is SqlExpression item
&& _sqlTranslator.Visit(parameterSource.ParameterExpression) is SqlParameterExpression sqlParameterExpression)
&& _sqlTranslator.Visit(parameterSource.ParameterExpression) is SqlParameterExpression sqlParameterExpression
&& !QueryCompilationContext.ParametersToNotConstantize.Contains(sqlParameterExpression.Name))
{
var inExpression = _sqlExpressionFactory.In(item, sqlParameterExpression);
var selectExpression = new SelectExpression(inExpression, _sqlAliasManager);
Expand Down Expand Up @@ -294,9 +296,13 @@ JsonScalarExpression jsonScalar

Check.DebugAssert(sqlParameterExpression is not null, "sqlParameterExpression is not null");

var tableAlias = _sqlAliasManager.GenerateTableAlias(sqlParameterExpression.Name.TrimStart('_'));
var primitiveCollectionsBehavior = RelationalOptionsExtension.Extract(QueryCompilationContext.ContextOptions)
.ParameterizedCollectionTranslationMode;

if (QueryCompilationContext.ParametersToConstantize.Contains(sqlParameterExpression.Name))
var tableAlias = _sqlAliasManager.GenerateTableAlias(sqlParameterExpression.Name.TrimStart('_'));
if (QueryCompilationContext.ParametersToConstantize.Contains(sqlParameterExpression.Name)
|| (primitiveCollectionsBehavior == ParameterizedCollectionTranslationMode.Constantize
&& !QueryCompilationContext.ParametersToNotConstantize.Contains(sqlParameterExpression.Name)))
{
var valuesExpression = new ValuesExpression(
tableAlias,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace Microsoft.EntityFrameworkCore.Query;
/// <inheritdoc />
public partial class RelationalShapedQueryCompilingExpressionVisitor : ShapedQueryCompilingExpressionVisitor
{
private readonly HashSet<string> _parametersToConstantize;
private readonly IReadOnlySet<string> _parametersToConstantize;
private readonly Type _contextType;
private readonly ISet<string> _tags;
private readonly bool _threadSafetyChecksEnabled;
Expand Down Expand Up @@ -53,7 +53,7 @@ public RelationalShapedQueryCompilingExpressionVisitor(
{
RelationalDependencies = relationalDependencies;

_parametersToConstantize = QueryCompilationContext.ParametersToConstantize;
_parametersToConstantize = (IReadOnlySet<string>)QueryCompilationContext.ParametersToConstantize;

_relationalParameterBasedSqlProcessor =
relationalDependencies.RelationalParameterBasedSqlProcessorFactory.Create(new RelationalParameterBasedSqlProcessorParameters(_useRelationalNulls, _parametersToConstantize));
Expand Down
2 changes: 1 addition & 1 deletion src/EFCore.Relational/Query/SqlNullabilityProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public SqlNullabilityProcessor(
/// <summary>
/// A collection of parameter names to constantize.
/// </summary>
protected virtual HashSet<string> ParametersToConstantize { get; }
protected virtual IReadOnlySet<string> ParametersToConstantize { get; }

/// <summary>
/// Dictionary of current parameter values in use.
Expand Down
8 changes: 4 additions & 4 deletions src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -944,9 +944,9 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCall)
}

argumentState = argumentState with { StateType = StateType.EvaluatableWithCapturedVariable };
var evaluatedArgument = ProcessEvaluatableRoot(argument, ref argumentState);
var evaluatedArgument = ProcessEvaluatableRoot(argument, ref argumentState, forceEvaluation: true);
_state = argumentState;
return evaluatedArgument;
return Call(method, evaluatedArgument);
}
}
}
Expand Down Expand Up @@ -1827,7 +1827,7 @@ private static StateType CombineStateTypes(StateType stateType1, StateType state
}

[return: NotNullIfNotNull(nameof(evaluatableRoot))]
private Expression? ProcessEvaluatableRoot(Expression? evaluatableRoot, ref State state)
private Expression? ProcessEvaluatableRoot(Expression? evaluatableRoot, ref State state, bool forceEvaluation = false)
{
if (evaluatableRoot is null)
{
Expand All @@ -1849,7 +1849,7 @@ private static StateType CombineStateTypes(StateType stateType1, StateType state
// 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))
if (!forceEvaluation && TryHandleNonEvaluatableAsRoot(evaluatableRoot, state, evaluateAsParameter, out var result))
{
return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,17 +112,29 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
return expression;
}

if (method.DeclaringType == typeof(EF)
&& method.Name == nameof(EF.Constant))
if (method.DeclaringType == typeof(EF))
{
if (!_isEfConstantSupported)
switch (method.Name)
{
throw new InvalidOperationException(CoreStrings.EFConstantNotSupported);
}
case nameof(EF.Constant):
{
if (!_isEfConstantSupported)
{
throw new InvalidOperationException(CoreStrings.EFConstantNotSupported);
}

var parameterExpression = (ParameterExpression)Visit(methodCallExpression.Arguments[0]);
_queryCompilationContext.ParametersToConstantize.Add(parameterExpression.Name!);
return parameterExpression;
var parameterExpression = (ParameterExpression)Visit(methodCallExpression.Arguments[0]);
_queryCompilationContext.ParametersToConstantize.Add(parameterExpression.Name!);
return parameterExpression;
}

case nameof(EF.Parameter):
{
var parameterExpression = (ParameterExpression)Visit(methodCallExpression.Arguments[0]);
_queryCompilationContext.ParametersToNotConstantize.Add(parameterExpression.Name!);
return parameterExpression;
}
}
}

// Normalize list[x] to list.ElementAt(x)
Expand Down
14 changes: 13 additions & 1 deletion src/EFCore/Query/QueryCompilationContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,19 @@ public class QueryCompilationContext
/// not used in application code.
/// </para>
/// </summary>
public virtual HashSet<string> ParametersToConstantize { get; } = new(StringComparer.Ordinal);
public virtual ISet<string> ParametersToConstantize { get; } = new HashSet<string>(StringComparer.Ordinal);

/// <summary>
/// <para>
/// Names of parameters on which <see cref="EF.Parameter{T}" /> was used. Such parameters are later not transformed into
/// constants even when parameterized collection constantization is configured as the default.
/// </para>
/// <para>
/// This property is typically used by database providers (and other extensions). It is generally
/// not used in application code.
/// </para>
/// </summary>
public virtual ISet<string> ParametersToNotConstantize { get; } = new HashSet<string>(StringComparer.Ordinal);

private static readonly IReadOnlySet<string> EmptySet = new HashSet<string>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,41 @@ FROM a IN (SELECT VALUE [@__i_0])
""");
});

public override Task Inline_collection_Contains_with_EF_Parameter(bool async)
=> CosmosTestHelpers.Instance.NoSyncTest(
async, async a =>
{
await base.Inline_collection_Contains_with_EF_Parameter(async);

AssertSql(
"""
@__p_0='[2,999,1000]'

SELECT VALUE c
FROM root c
WHERE ARRAY_CONTAINS(@__p_0, c["Id"])
""");
});

public override Task Inline_collection_Count_with_column_predicate_with_EF_Parameter(bool async)
=> CosmosTestHelpers.Instance.NoSyncTest(
async, async a =>
{
await base.Inline_collection_Count_with_column_predicate_with_EF_Parameter(async);

AssertSql(
"""
@__p_0='[2,999,1000]'

SELECT VALUE c
FROM root c
WHERE ((
SELECT VALUE COUNT(1)
FROM p IN (SELECT VALUE @__p_0)
WHERE (p > c["Id"])) = 2)
""");
});

public override Task Parameter_collection_Count(bool async)
=> CosmosTestHelpers.Instance.NoSyncTest(
async, async a =>
Expand Down
Loading