From 36460304a71a928ba654e4a5a0a7fd2f0af1ce57 Mon Sep 17 00:00:00 2001 From: Jiri Cincura Date: Wed, 9 Jul 2025 13:49:56 +0200 Subject: [PATCH 01/10] Small cleanup. --- .../Query/AdHocMiscellaneousQueryRelationalTestBase.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/EFCore.Relational.Specification.Tests/Query/AdHocMiscellaneousQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/AdHocMiscellaneousQueryRelationalTestBase.cs index 2496b692e1d..7476708138f 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/AdHocMiscellaneousQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/AdHocMiscellaneousQueryRelationalTestBase.cs @@ -20,6 +20,8 @@ protected void ClearLog() protected void AssertSql(params string[] expected) => TestSqlLoggerFactory.AssertBaseline(expected); + protected abstract DbContextOptionsBuilder SetParameterizedCollectionMode(DbContextOptionsBuilder optionsBuilder, ParameterizedCollectionMode parameterizedCollectionMode); + #region 2951 [ConditionalFact] @@ -268,8 +270,6 @@ public class Entity #region Inlined redacting - protected abstract DbContextOptionsBuilder SetParameterizedCollectionMode(DbContextOptionsBuilder optionsBuilder, ParameterizedCollectionMode parameterizedCollectionMode); - [ConditionalTheory] [MemberData(nameof(InlinedRedactingData))] public virtual async Task Check_inlined_constants_redacting(bool async, bool enableSensitiveDataLogging) From baa2de20f210801f6582f4940affe0f8232bdbb9 Mon Sep 17 00:00:00 2001 From: Jiri Cincura Date: Thu, 10 Jul 2025 11:17:51 +0200 Subject: [PATCH 02/10] Implement EF.MultipleParameters method. --- .../Internal/RelationalParameterProcessor.cs | 2 +- ...yableMethodTranslatingExpressionVisitor.cs | 12 +- ...lationalSqlTranslatingExpressionVisitor.cs | 4 +- .../SqlExpressions/SqlParameterExpression.cs | 14 +- .../Query/SqlNullabilityProcessor.cs | 38 ++--- .../SqlServerSqlNullabilityProcessor.cs | 10 +- src/EFCore/Properties/CoreStrings.Designer.cs | 30 ++-- src/EFCore/Properties/CoreStrings.resx | 13 +- .../Internal/ExpressionTreeFuncletizer.cs | 23 ++- ...yableMethodNormalizingExpressionVisitor.cs | 13 +- src/EFCore/Query/ParameterExpressionMode.cs | 25 +++ src/EFCore/Query/QueryParameterExpression.cs | 31 ++-- .../Query/NorthwindWhereQueryCosmosTest.cs | 4 +- ...itiveCollectionsQueryRelationalTestBase.cs | 115 +++++-------- .../NorthwindWhereQueryRelationalTestBase.cs | 14 ++ .../Query/NorthwindWhereQueryTestBase.cs | 4 +- ...dPrimitiveCollectionsQuerySqlServerTest.cs | 149 +++++++++++------ .../Query/NorthwindWhereQuerySqlServerTest.cs | 7 + ...aredPrimitiveCollectionsQuerySqliteTest.cs | 151 ++++++++++++------ 19 files changed, 391 insertions(+), 268 deletions(-) create mode 100644 src/EFCore/Query/ParameterExpressionMode.cs diff --git a/src/EFCore.Relational/Query/Internal/RelationalParameterProcessor.cs b/src/EFCore.Relational/Query/Internal/RelationalParameterProcessor.cs index b955ccae286..48bf0ae6bfe 100644 --- a/src/EFCore.Relational/Query/Internal/RelationalParameterProcessor.cs +++ b/src/EFCore.Relational/Query/Internal/RelationalParameterProcessor.cs @@ -128,7 +128,7 @@ private SqlParameterExpression VisitSqlParameter(SqlParameterExpression paramete uniquifiedName, parameter.Type, parameter.IsNullable, - parameter.ShouldBeConstantized, + parameter.ParameterExpressionMode, parameter.TypeMapping); return _sqlParameters[newParameter.InvariantName] = newParameter; diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index 848973552c6..a328de74fbc 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -244,7 +244,8 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp && methodCallExpression.Arguments[0] is ParameterQueryRootExpression parameterSource && TranslateExpression(methodCallExpression.Arguments[1]) is SqlExpression item && _sqlTranslator.Visit(parameterSource.QueryParameterExpression) is SqlParameterExpression sqlParameterExpression - && !parameterSource.QueryParameterExpression.ShouldNotBeConstantized) + && (parameterSource.QueryParameterExpression.ParameterExpressionMode is not ParameterExpressionMode.Parameter + && parameterSource.QueryParameterExpression.ParameterExpressionMode is not ParameterExpressionMode.MultipleParameters)) { var inExpression = _sqlExpressionFactory.In(item, sqlParameterExpression); var selectExpression = new SelectExpression(inExpression, _sqlAliasManager); @@ -298,11 +299,12 @@ JsonScalarExpression jsonScalar var tableAlias = _sqlAliasManager.GenerateTableAlias(sqlParameterExpression.Name.TrimStart('_')); - var constants = queryParameter.ShouldBeConstantized + var constants = queryParameter.ParameterExpressionMode is ParameterExpressionMode.Constants || (_parameterizedCollectionMode is ParameterizedCollectionMode.Constants - && !queryParameter.ShouldNotBeConstantized); - var multipleParameters = _parameterizedCollectionMode is ParameterizedCollectionMode.MultipleParameters - && !queryParameter.ShouldNotBeConstantized; + && queryParameter.ParameterExpressionMode is null); + var multipleParameters = queryParameter.ParameterExpressionMode is ParameterExpressionMode.MultipleParameters + || (_parameterizedCollectionMode is ParameterizedCollectionMode.MultipleParameters + && queryParameter.ParameterExpressionMode is null); if (constants || multipleParameters) { var valuesExpression = new ValuesExpression( diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs index c8f4ec36332..0e9c511f44d 100644 --- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs @@ -516,7 +516,7 @@ protected override Expression VisitExtension(Expression extensionExpression) name: queryParameter.Name, queryParameter.Type, nullable: false, - queryParameter.ShouldBeConstantized, + queryParameter.ParameterExpressionMode, typeMapping: null); } @@ -525,7 +525,7 @@ protected override Expression VisitExtension(Expression extensionExpression) name: queryParameter.Name, queryParameter.Type, queryParameter.Type.IsNullableType(), - queryParameter.ShouldBeConstantized, + queryParameter.ParameterExpressionMode, typeMapping: null); case StructuralTypeShaperExpression shaper: diff --git a/src/EFCore.Relational/Query/SqlExpressions/SqlParameterExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SqlParameterExpression.cs index 48c1602fbaa..53aab11f607 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SqlParameterExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SqlParameterExpression.cs @@ -17,7 +17,7 @@ public sealed class SqlParameterExpression : SqlExpression /// The of the expression. /// The associated with the expression. public SqlParameterExpression(string name, Type type, RelationalTypeMapping? typeMapping) - : this(invariantName: name, name: name, type.UnwrapNullableType(), type.IsNullableType(), shouldBeConstantized: false, typeMapping) + : this(invariantName: name, name: name, type.UnwrapNullableType(), type.IsNullableType(), parameterExpressionMode: null, typeMapping) { } @@ -31,21 +31,21 @@ public SqlParameterExpression(string name, Type type, RelationalTypeMapping? typ /// /// The of the expression. /// Whether this parameter can have null values. - /// Whether the user has indicated that this query parameter should be inlined as a constant. + /// How the parameter should be handled. /// The associated with the expression. public SqlParameterExpression( string invariantName, string name, Type type, bool nullable, - bool shouldBeConstantized, + ParameterExpressionMode? parameterExpressionMode, RelationalTypeMapping? typeMapping) : base(type.UnwrapNullableType(), typeMapping) { InvariantName = invariantName; Name = name; IsNullable = nullable; - ShouldBeConstantized = shouldBeConstantized; + ParameterExpressionMode = parameterExpressionMode; } /// @@ -65,9 +65,9 @@ public SqlParameterExpression( public bool IsNullable { get; } /// - /// Whether the user has indicated that this query parameter should be inlined as a constant. + /// How the parameter should be handled. /// - public bool ShouldBeConstantized { get; } + public ParameterExpressionMode? ParameterExpressionMode { get; } /// /// Applies supplied type mapping to this expression. @@ -75,7 +75,7 @@ public SqlParameterExpression( /// A relational type mapping to apply. /// A new expression which has supplied type mapping. public SqlExpression ApplyTypeMapping(RelationalTypeMapping? typeMapping) - => new SqlParameterExpression(InvariantName, Name, Type, IsNullable, ShouldBeConstantized, typeMapping); + => new SqlParameterExpression(InvariantName, Name, Type, IsNullable, ParameterExpressionMode, typeMapping); /// protected override Expression VisitChildren(ExpressionVisitor visitor) diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs index c4d182cb886..c0ec7c9cbfb 100644 --- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs +++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs @@ -127,10 +127,10 @@ protected override Expression VisitExtension(Expression node) var processedValues = new List(); - switch (ParameterizedCollectionMode) + switch (ParameterizedCollectionMode, valuesParameter.ParameterExpressionMode) { - case ParameterizedCollectionMode.MultipleParameters - when !valuesParameter.ShouldBeConstantized: + case (ParameterizedCollectionMode.MultipleParameters, null): + case (_, ParameterExpressionMode.MultipleParameters): { var expandedParameters = _collectionParameterExpansionMap.GetOrAddNew(valuesParameter); for (var i = 0; i < values.Count; i++) @@ -156,11 +156,8 @@ protected override Expression VisitExtension(Expression node) break; } - case ParameterizedCollectionMode.Constants: - case ParameterizedCollectionMode.Parameter - when valuesParameter.ShouldBeConstantized: - case ParameterizedCollectionMode.MultipleParameters - when valuesParameter.ShouldBeConstantized: + case (ParameterizedCollectionMode.Constants, null): + case (_, ParameterExpressionMode.Constants): { foreach (var value in values) { @@ -820,18 +817,15 @@ InExpression ProcessInExpressionValues( processedValues = []; - var useParameters = ParameterizedCollectionMode is ParameterizedCollectionMode.MultipleParameters - && !valuesParameter.ShouldBeConstantized; - var useConstants = - ParameterizedCollectionMode is ParameterizedCollectionMode.Constants - || - (ParameterizedCollectionMode is ParameterizedCollectionMode.Parameter - && valuesParameter.ShouldBeConstantized) - || - (ParameterizedCollectionMode is ParameterizedCollectionMode.MultipleParameters - && valuesParameter.ShouldBeConstantized); - var useParameter = ParameterizedCollectionMode is ParameterizedCollectionMode.Parameter - && !valuesParameter.ShouldBeConstantized; + var useMultipleParameters = valuesParameter.ParameterExpressionMode is ParameterExpressionMode.MultipleParameters + || (ParameterizedCollectionMode is ParameterizedCollectionMode.MultipleParameters + && valuesParameter.ParameterExpressionMode is null); + var useConstants = valuesParameter.ParameterExpressionMode is ParameterExpressionMode.Constants + || (ParameterizedCollectionMode is ParameterizedCollectionMode.Constants + && valuesParameter.ParameterExpressionMode is null); + var useParameter = valuesParameter.ParameterExpressionMode is ParameterExpressionMode.Parameter + || (ParameterizedCollectionMode is ParameterizedCollectionMode.Parameter + && valuesParameter.ParameterExpressionMode is null); var expandedParameters = _collectionParameterExpansionMap.GetOrAddNew(valuesParameter); var expandedParametersCounter = 0; for (var i = 0; i < values.Count; i++) @@ -842,7 +836,7 @@ ParameterizedCollectionMode is ParameterizedCollectionMode.Constants continue; } - switch (useParameters, useConstants, useParameter) + switch (useMultipleParameters, useConstants, useParameter) { case (true, false, false): // see #36311 for more info @@ -1426,7 +1420,7 @@ protected virtual SqlExpression VisitSqlParameter( nullable = false; - if (sqlParameterExpression.ShouldBeConstantized) + if (sqlParameterExpression.ParameterExpressionMode is ParameterExpressionMode.Constants) { var parameters = ParametersFacade.GetParametersAndDisableSqlCaching(); diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs index 8a97b4514f7..c08b2902da3 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs @@ -190,8 +190,9 @@ protected override Expression VisitExtension(Expression node) switch (node) { case ValuesExpression { ValuesParameter: SqlParameterExpression valuesParameter } valuesExpression - when ParameterizedCollectionMode is ParameterizedCollectionMode.MultipleParameters - && !valuesParameter.ShouldBeConstantized: + when valuesParameter.ParameterExpressionMode is ParameterExpressionMode.MultipleParameters + || (ParameterizedCollectionMode is ParameterizedCollectionMode.MultipleParameters + && valuesParameter.ParameterExpressionMode is null): { Check.DebugAssert(valuesParameter.TypeMapping is not null); Check.DebugAssert(valuesParameter.TypeMapping.ElementTypeMapping is not null); @@ -237,8 +238,9 @@ protected override SqlExpression VisitIn(InExpression inExpression, bool allowOp switch (inExpression.ValuesParameter) { case SqlParameterExpression valuesParameter - when ParameterizedCollectionMode is ParameterizedCollectionMode.MultipleParameters - && !valuesParameter.ShouldBeConstantized: + when valuesParameter.ParameterExpressionMode is ParameterExpressionMode.MultipleParameters + || (ParameterizedCollectionMode is ParameterizedCollectionMode.MultipleParameters + && valuesParameter.ParameterExpressionMode is null): { Check.DebugAssert(valuesParameter.TypeMapping is not null); Check.DebugAssert(valuesParameter.TypeMapping.ElementTypeMapping is not null); diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 61fb66db70f..9032f29286c 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -358,14 +358,6 @@ public static string CanOnlyConfigureExistingNavigations(object? navigationName, GetString("CanOnlyConfigureExistingNavigations", "0_navigationName", "1_entityType"), navigationName, entityType); - /// - /// The entity type '{entityType}' is configured to use the '{changeTrackingStrategy}' change tracking strategy, but does not implement the required '{notificationInterface}' interface. Implement '{notificationInterface}' on '{entityType}' or use a different change tracking strategy. - /// - public static string ChangeTrackingInterfaceMissing(object? entityType, object? changeTrackingStrategy, object? notificationInterface) - => string.Format( - GetString("ChangeTrackingInterfaceMissing", nameof(entityType), nameof(changeTrackingStrategy), nameof(notificationInterface)), - entityType, changeTrackingStrategy, notificationInterface); - /// /// Unable to save changes because a circular dependency was detected in the data to be saved: '{cycle}'. /// @@ -1013,10 +1005,12 @@ public static string EFConstantNotSupportedInPrecompiledQueries => GetString("EFConstantNotSupportedInPrecompiledQueries"); /// - /// 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 {methodName} 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 EFConstantWithNonEvaluatableArgument - => GetString("EFConstantWithNonEvaluatableArgument"); + public static string EFMethodWithNonEvaluatableArgument(object? methodName) + => string.Format( + GetString("EFMethodWithNonEvaluatableArgument", nameof(methodName)), + methodName); /// /// The EF.Parameter<T> method may only be used within Entity Framework LINQ queries. @@ -1024,12 +1018,6 @@ public static string EFConstantWithNonEvaluatableArgument 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. /// @@ -1340,6 +1328,14 @@ public static string GraphDoesNotContainVertex(object? vertex) public static string HiLoBadBlockSize => GetString("HiLoBadBlockSize"); + /// + /// The entity type '{entityType}' is configured to use the '{changeTrackingStrategy}' change tracking strategy, but does not implement the required '{notificationInterface}' interface. Implement '{notificationInterface}' on '{entityType}' or use a different change tracking strategy. + /// + public static string ChangeTrackingInterfaceMissing(object? entityType, object? changeTrackingStrategy, object? notificationInterface) + => string.Format( + GetString("ChangeTrackingInterfaceMissing", nameof(entityType), nameof(changeTrackingStrategy), nameof(notificationInterface)), + entityType, changeTrackingStrategy, notificationInterface); + /// /// A relationship cycle involving the primary keys of the following entity types was detected: '{entityType}'. This would prevent any entity to be inserted without violating the store constraints. Review the foreign keys defined on the primary keys and either remove or use other properties for at least one of them. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 9629cf01e81..1656f994a0e 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -243,9 +243,6 @@ Navigation '{1_entityType}.{0_navigationName}' was not found. Please add the navigation to the entity type before configuring it. - - The entity type '{entityType}' is configured to use the '{changeTrackingStrategy}' change tracking strategy, but does not implement the required '{notificationInterface}' interface. Implement '{notificationInterface}' on '{entityType}' or use a different change tracking strategy. - Unable to save changes because a circular dependency was detected in the data to be saved: '{cycle}'. @@ -495,15 +492,12 @@ The EF.Constant<T> method is not supported when using precompiled 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 {methodName} 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. @@ -624,6 +618,9 @@ The block size used for Hi-Lo value generation is not positive. The Hi-Lo generator is usually backed by a SQL sequence and this means that the sequence increment must be positive. + + The entity type '{entityType}' is configured to use the '{changeTrackingStrategy}' change tracking strategy, but does not implement the required '{notificationInterface}' interface. Implement '{notificationInterface}' on '{entityType}' or use a different change tracking strategy. + A relationship cycle involving the primary keys of the following entity types was detected: '{entityType}'. This would prevent any entity to be inserted without violating the store constraints. Review the foreign keys defined on the primary keys and either remove or use other properties for at least one of them. diff --git a/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs b/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs index 44fdec7dbc0..0618db01d2a 100644 --- a/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs +++ b/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs @@ -939,7 +939,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCall) if (!argumentState.IsEvaluatable) { - throw new InvalidOperationException(CoreStrings.EFConstantWithNonEvaluatableArgument); + throw new InvalidOperationException(CoreStrings.EFMethodWithNonEvaluatableArgument("EF.Constant")); } // Even EF.Constant will be parameter here. @@ -956,7 +956,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCall) if (!argumentState.IsEvaluatable) { - throw new InvalidOperationException(CoreStrings.EFParameterWithNonEvaluatableArgument); + throw new InvalidOperationException(CoreStrings.EFMethodWithNonEvaluatableArgument("EF.Parameter")); } argumentState = argumentState with { StateType = StateType.EvaluatableWithCapturedVariable }; @@ -967,6 +967,22 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCall) } } + // EF.MultipleParameters is defined in Relational, hence the hardcoded values here. + if (method.DeclaringType?.FullName == "Microsoft.EntityFrameworkCore.EFExtensions" && method.Name == "MultipleParameters") + { + var argument = Visit(methodCall.Arguments[0], out var argumentState); + + if (!argumentState.IsEvaluatable) + { + throw new InvalidOperationException(CoreStrings.EFMethodWithNonEvaluatableArgument("EF.MultipleParameters")); + } + + argumentState = argumentState with { StateType = StateType.EvaluatableWithCapturedVariable }; + var evaluatedArgument = ProcessEvaluatableRoot(argument, ref argumentState, forceEvaluation: true); + _state = argumentState; + return Call(method, evaluatedArgument); + } + // .NET 10 made changes to overload resolution to prefer Span-based overloads when those exist ("first-class spans"). // Unfortunately, the LINQ interpreter does not support ref structs, so we rewrite e.g. MemoryExtensions.Contains to // Enumerable.Contains here. See https://github.com/dotnet/runtime/issues/109757,. @@ -1977,8 +1993,7 @@ private static StateType CombineStateTypes(StateType stateType1, StateType state return _parameterizedValues[evaluatableRoot] = new QueryParameterExpression( parameterName, evaluatableRoot.Type, - shouldBeConstantized: false, - shouldNotBeConstantized: false, + parameterExpressionMode: null, isNonNullableReferenceType); } diff --git a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs index c89c7f3d357..a1d094671aa 100644 --- a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs @@ -128,7 +128,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp var queryParameter = (QueryParameterExpression)Visit(methodCallExpression.Arguments[0]); return new QueryParameterExpression( - queryParameter.Name, queryParameter.Type, shouldBeConstantized: true, shouldNotBeConstantized: false, + queryParameter.Name, queryParameter.Type, parameterExpressionMode: ParameterExpressionMode.Constants, queryParameter.IsNonNullableReferenceType); } @@ -136,12 +136,21 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp { var queryParameter = (QueryParameterExpression)Visit(methodCallExpression.Arguments[0]); return new QueryParameterExpression( - queryParameter.Name, queryParameter.Type, shouldBeConstantized: false, shouldNotBeConstantized: true, + queryParameter.Name, queryParameter.Type, parameterExpressionMode: ParameterExpressionMode.Parameter, queryParameter.IsNonNullableReferenceType); } } } + // EF.MultipleParameters is defined in Relational, hence the hardcoded values here. + if (method.DeclaringType?.FullName == "Microsoft.EntityFrameworkCore.EFExtensions" && method.Name == "MultipleParameters") + { + var queryParameter = (QueryParameterExpression)Visit(methodCallExpression.Arguments[0]); + return new QueryParameterExpression( + queryParameter.Name, queryParameter.Type, parameterExpressionMode: ParameterExpressionMode.MultipleParameters, + queryParameter.IsNonNullableReferenceType); + } + // Normalize list[x] to list.ElementAt(x) if (methodCallExpression is { diff --git a/src/EFCore/Query/ParameterExpressionMode.cs b/src/EFCore/Query/ParameterExpressionMode.cs new file mode 100644 index 00000000000..6ebf027270e --- /dev/null +++ b/src/EFCore/Query/ParameterExpressionMode.cs @@ -0,0 +1,25 @@ +// 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.Query; + +/// +/// Describes how the parameter expression in a query should be handled. +/// +public enum ParameterExpressionMode +{ + /// + /// Handle as constants. + /// + Constants, + + /// + /// Handle as parameter. + /// + Parameter, + + /// + /// Handle as multiple parameters. + /// + MultipleParameters, +} diff --git a/src/EFCore/Query/QueryParameterExpression.cs b/src/EFCore/Query/QueryParameterExpression.cs index 696ff1f3b16..7e283b2c3d4 100644 --- a/src/EFCore/Query/QueryParameterExpression.cs +++ b/src/EFCore/Query/QueryParameterExpression.cs @@ -14,31 +14,30 @@ namespace Microsoft.EntityFrameworkCore.Query; public class QueryParameterExpression : Expression, IPrintableExpression { /// - /// Creates a new instance of the class with associated query provider. + /// Creates a new instance of the class with associated query provider. /// public QueryParameterExpression(string name, Type type) - : this(name, type, shouldBeConstantized: false, shouldNotBeConstantized: false, isNonNullableReferenceType: false) + : this(name, type, parameterExpressionMode: null, isNonNullableReferenceType: false) { } /// - /// Creates a new instance of the class with associated query provider. + /// Creates a new instance of the class with associated query provider. /// - public QueryParameterExpression(string name, Type type, bool shouldBeConstantized, bool shouldNotBeConstantized) - : this(name, type, shouldBeConstantized, shouldNotBeConstantized, isNonNullableReferenceType: false) + public QueryParameterExpression(string name, Type type, ParameterExpressionMode parameterExpressionMode) + : this(name, type, parameterExpressionMode, isNonNullableReferenceType: false) { } /// - /// Creates a new instance of the class with associated query provider. + /// Creates a new instance of the class with associated query provider. /// [Experimental(EFDiagnostics.PrecompiledQueryExperimental)] - public QueryParameterExpression(string name, Type type, bool shouldBeConstantized, bool shouldNotBeConstantized, bool isNonNullableReferenceType) + public QueryParameterExpression(string name, Type type, ParameterExpressionMode? parameterExpressionMode, bool isNonNullableReferenceType) { Name = name; Type = type; - ShouldBeConstantized = shouldBeConstantized; - ShouldNotBeConstantized = shouldNotBeConstantized; + ParameterExpressionMode = parameterExpressionMode; IsNonNullableReferenceType = isNonNullableReferenceType; } @@ -61,14 +60,9 @@ public QueryParameterExpression(string name, Type type, bool shouldBeConstantize public virtual bool IsNonNullableReferenceType { get; } /// - /// Whether the user has indicated that this query parameter should be inlined as a constant. + /// How should the parameter be handled. /// - public virtual bool ShouldBeConstantized { get; } - - /// - /// Whether the user has indicated that this query parameter shouldn't be inlined as a constant. - /// - public virtual bool ShouldNotBeConstantized { get; } + public virtual ParameterExpressionMode? ParameterExpressionMode { get; } /// public override ExpressionType NodeType @@ -92,11 +86,10 @@ public override bool Equals(object? obj) private bool Equals(QueryParameterExpression queryParameterExpression) => Name == queryParameterExpression.Name && Type == queryParameterExpression.Type - && ShouldBeConstantized == queryParameterExpression.ShouldBeConstantized - && ShouldNotBeConstantized == queryParameterExpression.ShouldNotBeConstantized + && ParameterExpressionMode == queryParameterExpression.ParameterExpressionMode && IsNonNullableReferenceType == queryParameterExpression.IsNonNullableReferenceType; /// public override int GetHashCode() - => HashCode.Combine(Name, Type, ShouldBeConstantized, ShouldNotBeConstantized, IsNonNullableReferenceType); + => HashCode.Combine(Name, Type, ParameterExpressionMode, IsNonNullableReferenceType); } diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs index cef8b19a3aa..3c3f5196465 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindWhereQueryCosmosTest.cs @@ -2339,8 +2339,8 @@ public override async Task EF_Constant_does_not_parameterized_as_part_of_bigger_ public override async Task EF_Constant_with_non_evaluatable_argument_throws(bool async) { await base.EF_Constant_with_non_evaluatable_argument_throws(async); - AssertSql( - ); + + AssertSql(); } public override Task EF_Parameter(bool async) diff --git a/test/EFCore.Relational.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryRelationalTestBase.cs index 2cc15f6fafd..0904f628dd5 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryRelationalTestBase.cs @@ -36,11 +36,15 @@ public virtual async Task Column_collection_inside_json_owned_entity() Assert.Equivalent(new[] { "foo", "bar" }, result.Owned.Strings); } - [ConditionalFact] - public virtual async Task Parameter_collection_Count_with_column_predicate_with_default_constants() + protected static IEnumerable ParameterizedCollectionModeValues() + => Enum.GetValues().Select(x => [x]); + + [ConditionalTheory] + [MemberData(nameof(ParameterizedCollectionModeValues))] + public virtual async Task Parameter_collection_Count_with_column_predicate_with_default_mode(ParameterizedCollectionMode mode) { var contextFactory = await InitializeAsync( - onConfiguring: b => SetParameterizedCollectionMode(b, ParameterizedCollectionMode.Constants), + onConfiguring: b => SetParameterizedCollectionMode(b, mode), seed: context => { context.AddRange( @@ -56,11 +60,12 @@ public virtual async Task Parameter_collection_Count_with_column_predicate_with_ Assert.Equivalent(new[] { 100 }, result); } - [ConditionalFact] - public virtual async Task Parameter_collection_Contains_with_default_constants() + [ConditionalTheory] + [MemberData(nameof(ParameterizedCollectionModeValues))] + public virtual async Task Parameter_collection_Contains_with_default_mode(ParameterizedCollectionMode mode) { var contextFactory = await InitializeAsync( - onConfiguring: b => SetParameterizedCollectionMode(b, ParameterizedCollectionMode.Constants), + onConfiguring: b => SetParameterizedCollectionMode(b, mode), seed: context => { context.AddRange( @@ -77,11 +82,12 @@ public virtual async Task Parameter_collection_Contains_with_default_constants() Assert.Equivalent(new[] { 2 }, result); } - [ConditionalFact] - public virtual async Task Parameter_collection_Count_with_column_predicate_with_default_constants_EF_Parameter() + [ConditionalTheory] + [MemberData(nameof(ParameterizedCollectionModeValues))] + public virtual async Task Parameter_collection_Count_with_column_predicate_with_default_mode_EF_Constant(ParameterizedCollectionMode mode) { var contextFactory = await InitializeAsync( - onConfiguring: b => SetParameterizedCollectionMode(b, ParameterizedCollectionMode.Constants), + onConfiguring: b => SetParameterizedCollectionMode(b, mode), seed: context => { context.AddRange( @@ -93,57 +99,16 @@ public virtual async Task Parameter_collection_Count_with_column_predicate_with_ await using var context = contextFactory.CreateContext(); var ids = new[] { 2, 999 }; - var result = await context.Set().Where(c => EF.Parameter(ids).Count(i => i > c.Id) == 1).Select(x => x.Id) - .ToListAsync(); - Assert.Equivalent(new[] { 100 }, result); - } - - [ConditionalFact] - public virtual async Task Parameter_collection_Contains_with_default_constants_EF_Parameter() - { - var contextFactory = await InitializeAsync( - onConfiguring: b => SetParameterizedCollectionMode(b, ParameterizedCollectionMode.Constants), - seed: context => - { - context.AddRange( - new TestEntity { Id = 1 }, - new TestEntity { Id = 2 }, - new TestEntity { Id = 100 }); - return context.SaveChangesAsync(); - }); - - await using var context = contextFactory.CreateContext(); - - var ints = new[] { 2, 999 }; - var result = await context.Set().Where(c => EF.Parameter(ints).Contains(c.Id)).Select(x => x.Id).ToListAsync(); - Assert.Equivalent(new[] { 2 }, result); - } - - [ConditionalFact] - public virtual async Task Parameter_collection_Count_with_column_predicate_with_default_parameter() - { - var contextFactory = await InitializeAsync( - onConfiguring: b => SetParameterizedCollectionMode(b, ParameterizedCollectionMode.Parameter), - seed: context => - { - context.AddRange( - new TestEntity { Id = 1 }, - new TestEntity { Id = 100 }); - return context.SaveChangesAsync(); - }); - - await using var context = contextFactory.CreateContext(); - - var ids = new[] { 2, 999 }; - var result = await context.Set().Where(c => ids.Count(i => i > c.Id) == 1).Select(x => x.Id).ToListAsync(); + var result = await context.Set().Where(c => EF.Constant(ids).Count(i => i > c.Id) == 1).Select(x => x.Id).ToListAsync(); Assert.Equivalent(new[] { 100 }, result); } - [ConditionalFact] - public virtual async Task Parameter_collection_Contains_with_default_parameter() + [ConditionalTheory] + [MemberData(nameof(ParameterizedCollectionModeValues))] + public virtual async Task Parameter_collection_Contains_with_default_mode_EF_Constant(ParameterizedCollectionMode mode) { var contextFactory = await InitializeAsync( - onConfiguring: b => SetParameterizedCollectionMode(b, ParameterizedCollectionMode.Parameter), + onConfiguring: b => SetParameterizedCollectionMode(b, mode), seed: context => { context.AddRange( @@ -156,15 +121,16 @@ public virtual async Task Parameter_collection_Contains_with_default_parameter() await using var context = contextFactory.CreateContext(); var ints = new[] { 2, 999 }; - var result = await context.Set().Where(c => ints.Contains(c.Id)).Select(x => x.Id).ToListAsync(); + var result = await context.Set().Where(c => EF.Constant(ints).Contains(c.Id)).Select(x => x.Id).ToListAsync(); Assert.Equivalent(new[] { 2 }, result); } - [ConditionalFact] - public virtual async Task Parameter_collection_Count_with_column_predicate_with_default_parameter_EF_Constant() + [ConditionalTheory] + [MemberData(nameof(ParameterizedCollectionModeValues))] + public virtual async Task Parameter_collection_Count_with_column_predicate_with_default_mode_EF_Parameter(ParameterizedCollectionMode mode) { var contextFactory = await InitializeAsync( - onConfiguring: b => SetParameterizedCollectionMode(b, ParameterizedCollectionMode.Parameter), + onConfiguring: b => SetParameterizedCollectionMode(b, mode), seed: context => { context.AddRange( @@ -176,15 +142,17 @@ public virtual async Task Parameter_collection_Count_with_column_predicate_with_ await using var context = contextFactory.CreateContext(); var ids = new[] { 2, 999 }; - var result = await context.Set().Where(c => EF.Constant(ids).Count(i => i > c.Id) == 1).Select(x => x.Id).ToListAsync(); + var result = await context.Set().Where(c => EF.Parameter(ids).Count(i => i > c.Id) == 1).Select(x => x.Id) + .ToListAsync(); Assert.Equivalent(new[] { 100 }, result); } - [ConditionalFact] - public virtual async Task Parameter_collection_Contains_with_default_parameter_EF_Constant() + [ConditionalTheory] + [MemberData(nameof(ParameterizedCollectionModeValues))] + public virtual async Task Parameter_collection_Contains_with_default_mode_EF_Parameter(ParameterizedCollectionMode mode) { var contextFactory = await InitializeAsync( - onConfiguring: b => SetParameterizedCollectionMode(b, ParameterizedCollectionMode.Parameter), + onConfiguring: b => SetParameterizedCollectionMode(b, mode), seed: context => { context.AddRange( @@ -197,15 +165,16 @@ public virtual async Task Parameter_collection_Contains_with_default_parameter_E await using var context = contextFactory.CreateContext(); var ints = new[] { 2, 999 }; - var result = await context.Set().Where(c => EF.Constant(ints).Contains(c.Id)).Select(x => x.Id).ToListAsync(); + var result = await context.Set().Where(c => EF.Parameter(ints).Contains(c.Id)).Select(x => x.Id).ToListAsync(); Assert.Equivalent(new[] { 2 }, result); } - [ConditionalFact] - public virtual async Task Parameter_collection_Count_with_column_predicate_with_default_multiple_parameters() + [ConditionalTheory] + [MemberData(nameof(ParameterizedCollectionModeValues))] + public virtual async Task Parameter_collection_Count_with_column_predicate_with_default_mode_EF_MultipleParameters(ParameterizedCollectionMode mode) { var contextFactory = await InitializeAsync( - onConfiguring: b => SetParameterizedCollectionMode(b, ParameterizedCollectionMode.MultipleParameters), + onConfiguring: b => SetParameterizedCollectionMode(b, mode), seed: context => { context.AddRange( @@ -217,15 +186,17 @@ public virtual async Task Parameter_collection_Count_with_column_predicate_with_ await using var context = contextFactory.CreateContext(); var ids = new[] { 2, 999 }; - var result = await context.Set().Where(c => ids.Count(i => i > c.Id) == 1).Select(x => x.Id).ToListAsync(); + var result = await context.Set().Where(c => EF.MultipleParameters(ids).Count(i => i > c.Id) == 1).Select(x => x.Id) + .ToListAsync(); Assert.Equivalent(new[] { 100 }, result); } - [ConditionalFact] - public virtual async Task Parameter_collection_Contains_with_default_multiple_parameters() + [ConditionalTheory] + [MemberData(nameof(ParameterizedCollectionModeValues))] + public virtual async Task Parameter_collection_Contains_with_default_mode_EF_MultipleParameters(ParameterizedCollectionMode mode) { var contextFactory = await InitializeAsync( - onConfiguring: b => SetParameterizedCollectionMode(b, ParameterizedCollectionMode.MultipleParameters), + onConfiguring: b => SetParameterizedCollectionMode(b, mode), seed: context => { context.AddRange( @@ -238,7 +209,7 @@ public virtual async Task Parameter_collection_Contains_with_default_multiple_pa await using var context = contextFactory.CreateContext(); var ints = new[] { 2, 999 }; - var result = await context.Set().Where(c => ints.Contains(c.Id)).Select(x => x.Id).ToListAsync(); + var result = await context.Set().Where(c => EF.MultipleParameters(ints).Contains(c.Id)).Select(x => x.Id).ToListAsync(); Assert.Equivalent(new[] { 2 }, result); } diff --git a/test/EFCore.Relational.Specification.Tests/Query/NorthwindWhereQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/NorthwindWhereQueryRelationalTestBase.cs index 819f1a58c67..26b62b45334 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/NorthwindWhereQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/NorthwindWhereQueryRelationalTestBase.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 Microsoft.EntityFrameworkCore.TestModels.Northwind; + namespace Microsoft.EntityFrameworkCore.Query; #nullable disable @@ -11,6 +13,18 @@ public abstract class NorthwindWhereQueryRelationalTestBase(TFixture f public override Task Where_bool_client_side_negated(bool async) => AssertTranslationFailed(() => base.Where_bool_client_side_negated(async)); + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task EF_MultipleParameters_with_non_evaluatable_argument_throws(bool async) + { + var exception = await Assert.ThrowsAsync( + () => AssertQuery( + async, + ss => ss.Set().Where(c => c.Orders == EF.MultipleParameters(c.Orders)))); + + Assert.Equal(CoreStrings.EFMethodWithNonEvaluatableArgument("EF.MultipleParameters"), exception.Message); + } + protected override QueryAsserter CreateQueryAsserter(TFixture fixture) => new RelationalQueryAsserter( fixture, RewriteExpectedQueryExpression, RewriteServerQueryExpression); diff --git a/test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs index 76b0ad64190..a3256aa31b7 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs @@ -2082,7 +2082,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.EFConstantWithNonEvaluatableArgument, exception.Message); + Assert.Equal(CoreStrings.EFMethodWithNonEvaluatableArgument("EF.Constant"), exception.Message); } [ConditionalTheory] @@ -2129,7 +2129,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.EFParameterWithNonEvaluatableArgument, exception.Message); + Assert.Equal(CoreStrings.EFMethodWithNonEvaluatableArgument("EF.Parameter"), exception.Message); } private class EntityWithImplicitCast(int value) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs index 3016f7c7b93..823ef7284d7 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs @@ -785,12 +785,16 @@ FROM [TestEntityWithOwned] AS [t] """); } - public override async Task Parameter_collection_Count_with_column_predicate_with_default_constants() + public override async Task Parameter_collection_Count_with_column_predicate_with_default_mode(ParameterizedCollectionMode mode) { - await base.Parameter_collection_Count_with_column_predicate_with_default_constants(); + await base.Parameter_collection_Count_with_column_predicate_with_default_mode(mode); - AssertSql( - """ + switch (mode) + { + case ParameterizedCollectionMode.Constants: + { + AssertSql( + """ SELECT [t].[Id] FROM [TestEntity] AS [t] WHERE ( @@ -798,43 +802,68 @@ SELECT COUNT(*) FROM (VALUES (2), (999)) AS [i]([Value]) WHERE [i].[Value] > [t].[Id]) = 1 """); - } + break; + } - public override async Task Parameter_collection_Contains_with_default_constants() - { - await base.Parameter_collection_Contains_with_default_constants(); + case ParameterizedCollectionMode.Parameter: + { + AssertSql( + """ +@ids='[2,999]' (Size = 4000) - AssertSql( - """ SELECT [t].[Id] FROM [TestEntity] AS [t] -WHERE [t].[Id] IN (2, 999) +WHERE ( + SELECT COUNT(*) + FROM OPENJSON(@ids) WITH ([value] int '$') AS [i] + WHERE [i].[value] > [t].[Id]) = 1 """); - } - - public override async Task Parameter_collection_Count_with_column_predicate_with_default_constants_EF_Parameter() - { - await base.Parameter_collection_Count_with_column_predicate_with_default_constants_EF_Parameter(); + break; + } - AssertSql( - """ -@ids='[2,999]' (Size = 4000) + case ParameterizedCollectionMode.MultipleParameters: + { + AssertSql( + """ +@ids1='2' +@ids2='999' SELECT [t].[Id] FROM [TestEntity] AS [t] WHERE ( SELECT COUNT(*) - FROM OPENJSON(@ids) WITH ([value] int '$') AS [i] - WHERE [i].[value] > [t].[Id]) = 1 + FROM (VALUES (@ids1), (@ids2)) AS [i]([Value]) + WHERE [i].[Value] > [t].[Id]) = 1 """); + break; + } + + default: + throw new NotImplementedException(); + } } - public override async Task Parameter_collection_Contains_with_default_constants_EF_Parameter() + public override async Task Parameter_collection_Contains_with_default_mode(ParameterizedCollectionMode mode) { - await base.Parameter_collection_Contains_with_default_constants_EF_Parameter(); + await base.Parameter_collection_Contains_with_default_mode(mode); - AssertSql( - """ + switch (mode) + { + case ParameterizedCollectionMode.Constants: + { + AssertSql( + """ +SELECT [t].[Id] +FROM [TestEntity] AS [t] +WHERE [t].[Id] IN (2, 999) +"""); + break; + } + + case ParameterizedCollectionMode.Parameter: + { + AssertSql( + """ @ints='[2,999]' (Size = 4000) SELECT [t].[Id] @@ -844,72 +873,92 @@ SELECT [i].[value] FROM OPENJSON(@ints) WITH ([value] int '$') AS [i] ) """); + break; + } + + case ParameterizedCollectionMode.MultipleParameters: + { + AssertSql( + """ +@ints1='2' +@ints2='999' + +SELECT [t].[Id] +FROM [TestEntity] AS [t] +WHERE [t].[Id] IN (@ints1, @ints2) +"""); + break; + } + + default: + throw new NotImplementedException(); + } } - public override async Task Parameter_collection_Count_with_column_predicate_with_default_parameter() + public override async Task Parameter_collection_Count_with_column_predicate_with_default_mode_EF_Constant(ParameterizedCollectionMode mode) { - await base.Parameter_collection_Count_with_column_predicate_with_default_parameter(); + await base.Parameter_collection_Count_with_column_predicate_with_default_mode_EF_Constant(mode); AssertSql( """ -@ids='[2,999]' (Size = 4000) - SELECT [t].[Id] FROM [TestEntity] AS [t] WHERE ( SELECT COUNT(*) - FROM OPENJSON(@ids) WITH ([value] int '$') AS [i] - WHERE [i].[value] > [t].[Id]) = 1 + FROM (VALUES (2), (999)) AS [i]([Value]) + WHERE [i].[Value] > [t].[Id]) = 1 """); } - public override async Task Parameter_collection_Contains_with_default_parameter() + public override async Task Parameter_collection_Contains_with_default_mode_EF_Constant(ParameterizedCollectionMode mode) { - await base.Parameter_collection_Contains_with_default_parameter(); + await base.Parameter_collection_Contains_with_default_mode_EF_Constant(mode); AssertSql( """ -@ints='[2,999]' (Size = 4000) - SELECT [t].[Id] FROM [TestEntity] AS [t] -WHERE [t].[Id] IN ( - SELECT [i].[value] - FROM OPENJSON(@ints) WITH ([value] int '$') AS [i] -) +WHERE [t].[Id] IN (2, 999) """); } - public override async Task Parameter_collection_Count_with_column_predicate_with_default_parameter_EF_Constant() + public override async Task Parameter_collection_Count_with_column_predicate_with_default_mode_EF_Parameter(ParameterizedCollectionMode mode) { - await base.Parameter_collection_Count_with_column_predicate_with_default_parameter_EF_Constant(); + await base.Parameter_collection_Count_with_column_predicate_with_default_mode_EF_Parameter(mode); AssertSql( """ +@ids='[2,999]' (Size = 4000) + SELECT [t].[Id] FROM [TestEntity] AS [t] WHERE ( SELECT COUNT(*) - FROM (VALUES (2), (999)) AS [i]([Value]) - WHERE [i].[Value] > [t].[Id]) = 1 + FROM OPENJSON(@ids) WITH ([value] int '$') AS [i] + WHERE [i].[value] > [t].[Id]) = 1 """); } - public override async Task Parameter_collection_Contains_with_default_parameter_EF_Constant() + public override async Task Parameter_collection_Contains_with_default_mode_EF_Parameter(ParameterizedCollectionMode mode) { - await base.Parameter_collection_Contains_with_default_parameter_EF_Constant(); + await base.Parameter_collection_Contains_with_default_mode_EF_Parameter(mode); AssertSql( """ +@ints='[2,999]' (Size = 4000) + SELECT [t].[Id] FROM [TestEntity] AS [t] -WHERE [t].[Id] IN (2, 999) +WHERE [t].[Id] IN ( + SELECT [i].[value] + FROM OPENJSON(@ints) WITH ([value] int '$') AS [i] +) """); } - public override async Task Parameter_collection_Count_with_column_predicate_with_default_multiple_parameters() + public override async Task Parameter_collection_Count_with_column_predicate_with_default_mode_EF_MultipleParameters(ParameterizedCollectionMode mode) { - await base.Parameter_collection_Count_with_column_predicate_with_default_multiple_parameters(); + await base.Parameter_collection_Count_with_column_predicate_with_default_mode_EF_MultipleParameters(mode); AssertSql( """ @@ -925,9 +974,9 @@ SELECT COUNT(*) """); } - public override async Task Parameter_collection_Contains_with_default_multiple_parameters() + public override async Task Parameter_collection_Contains_with_default_mode_EF_MultipleParameters(ParameterizedCollectionMode mode) { - await base.Parameter_collection_Contains_with_default_multiple_parameters(); + await base.Parameter_collection_Contains_with_default_mode_EF_MultipleParameters(mode); AssertSql( """ diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs index f0f1f20ff23..bb81e7cd85e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindWhereQuerySqlServerTest.cs @@ -2970,6 +2970,13 @@ FROM [Orders] AS [o] """); } + public override async Task EF_MultipleParameters_with_non_evaluatable_argument_throws(bool async) + { + await base.EF_MultipleParameters_with_non_evaluatable_argument_throws(async); + + AssertSql(); + } + #region Evaluation order of operators public override async Task Take_and_Where_evaluation_order(bool async) diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqliteTest.cs index fbddf89fa4f..9cb695876e1 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqliteTest.cs @@ -329,12 +329,16 @@ LIMIT 2 """); } - public override async Task Parameter_collection_Count_with_column_predicate_with_default_constants() + public override async Task Parameter_collection_Count_with_column_predicate_with_default_mode(ParameterizedCollectionMode mode) { - await base.Parameter_collection_Count_with_column_predicate_with_default_constants(); - - AssertSql( - """ + await base.Parameter_collection_Count_with_column_predicate_with_default_mode(mode); + + switch (mode) + { + case ParameterizedCollectionMode.Constants: + { + AssertSql( + """ SELECT "t"."Id" FROM "TestEntity" AS "t" WHERE ( @@ -342,43 +346,68 @@ SELECT COUNT(*) FROM (SELECT 2 AS "Value" UNION ALL VALUES (999)) AS "i" WHERE "i"."Value" > "t"."Id") = 1 """); - } + break; + } - public override async Task Parameter_collection_Contains_with_default_constants() - { - await base.Parameter_collection_Contains_with_default_constants(); + case ParameterizedCollectionMode.Parameter: + { + AssertSql( + """ +@ids='[2,999]' (Size = 7) - AssertSql( - """ SELECT "t"."Id" FROM "TestEntity" AS "t" -WHERE "t"."Id" IN (2, 999) +WHERE ( + SELECT COUNT(*) + FROM json_each(@ids) AS "i" + WHERE "i"."value" > "t"."Id") = 1 """); - } - - public override async Task Parameter_collection_Count_with_column_predicate_with_default_constants_EF_Parameter() - { - await base.Parameter_collection_Count_with_column_predicate_with_default_constants_EF_Parameter(); + break; + } - AssertSql( - """ -@ids='[2,999]' (Size = 7) + case ParameterizedCollectionMode.MultipleParameters: + { + AssertSql( + """ +@ids1='2' +@ids2='999' SELECT "t"."Id" FROM "TestEntity" AS "t" WHERE ( SELECT COUNT(*) - FROM json_each(@ids) AS "i" - WHERE "i"."value" > "t"."Id") = 1 + FROM (SELECT @ids1 AS "Value" UNION ALL VALUES (@ids2)) AS "i" + WHERE "i"."Value" > "t"."Id") = 1 """); + break; + } + + default: + throw new NotImplementedException(); + } } - public override async Task Parameter_collection_Contains_with_default_constants_EF_Parameter() + public override async Task Parameter_collection_Contains_with_default_mode(ParameterizedCollectionMode mode) { - await base.Parameter_collection_Contains_with_default_constants_EF_Parameter(); + await base.Parameter_collection_Contains_with_default_mode(mode); + + switch (mode) + { + case ParameterizedCollectionMode.Constants: + { + AssertSql( + """ +SELECT "t"."Id" +FROM "TestEntity" AS "t" +WHERE "t"."Id" IN (2, 999) +"""); + break; + } - AssertSql( - """ + case ParameterizedCollectionMode.Parameter: + { + AssertSql( + """ @ints='[2,999]' (Size = 7) SELECT "t"."Id" @@ -388,72 +417,92 @@ public override async Task Parameter_collection_Contains_with_default_constants_ FROM json_each(@ints) AS "i" ) """); + break; + } + + case ParameterizedCollectionMode.MultipleParameters: + { + AssertSql( + """ +@ints1='2' +@ints2='999' + +SELECT "t"."Id" +FROM "TestEntity" AS "t" +WHERE "t"."Id" IN (@ints1, @ints2) +"""); + break; + } + + default: + throw new NotImplementedException(); + } } - public override async Task Parameter_collection_Count_with_column_predicate_with_default_parameter() + public override async Task Parameter_collection_Count_with_column_predicate_with_default_mode_EF_Constant(ParameterizedCollectionMode mode) { - await base.Parameter_collection_Count_with_column_predicate_with_default_parameter(); + await base.Parameter_collection_Count_with_column_predicate_with_default_mode_EF_Constant(mode); AssertSql( """ -@ids='[2,999]' (Size = 7) - SELECT "t"."Id" FROM "TestEntity" AS "t" WHERE ( SELECT COUNT(*) - FROM json_each(@ids) AS "i" - WHERE "i"."value" > "t"."Id") = 1 + FROM (SELECT 2 AS "Value" UNION ALL VALUES (999)) AS "i" + WHERE "i"."Value" > "t"."Id") = 1 """); } - public override async Task Parameter_collection_Contains_with_default_parameter() + public override async Task Parameter_collection_Contains_with_default_mode_EF_Constant(ParameterizedCollectionMode mode) { - await base.Parameter_collection_Contains_with_default_parameter(); + await base.Parameter_collection_Contains_with_default_mode_EF_Constant(mode); AssertSql( """ -@ints='[2,999]' (Size = 7) - SELECT "t"."Id" FROM "TestEntity" AS "t" -WHERE "t"."Id" IN ( - SELECT "i"."value" - FROM json_each(@ints) AS "i" -) +WHERE "t"."Id" IN (2, 999) """); } - public override async Task Parameter_collection_Count_with_column_predicate_with_default_parameter_EF_Constant() + public override async Task Parameter_collection_Count_with_column_predicate_with_default_mode_EF_Parameter(ParameterizedCollectionMode mode) { - await base.Parameter_collection_Count_with_column_predicate_with_default_parameter_EF_Constant(); + await base.Parameter_collection_Count_with_column_predicate_with_default_mode_EF_Parameter(mode); AssertSql( """ +@ids='[2,999]' (Size = 7) + SELECT "t"."Id" FROM "TestEntity" AS "t" WHERE ( SELECT COUNT(*) - FROM (SELECT 2 AS "Value" UNION ALL VALUES (999)) AS "i" - WHERE "i"."Value" > "t"."Id") = 1 + FROM json_each(@ids) AS "i" + WHERE "i"."value" > "t"."Id") = 1 """); } - public override async Task Parameter_collection_Contains_with_default_parameter_EF_Constant() + public override async Task Parameter_collection_Contains_with_default_mode_EF_Parameter(ParameterizedCollectionMode mode) { - await base.Parameter_collection_Contains_with_default_parameter_EF_Constant(); + await base.Parameter_collection_Contains_with_default_mode_EF_Parameter(mode); AssertSql( """ +@ints='[2,999]' (Size = 7) + SELECT "t"."Id" FROM "TestEntity" AS "t" -WHERE "t"."Id" IN (2, 999) +WHERE "t"."Id" IN ( + SELECT "i"."value" + FROM json_each(@ints) AS "i" +) """); } - public override async Task Parameter_collection_Count_with_column_predicate_with_default_multiple_parameters() + public override async Task Parameter_collection_Count_with_column_predicate_with_default_mode_EF_MultipleParameters(ParameterizedCollectionMode mode) { - await base.Parameter_collection_Count_with_column_predicate_with_default_multiple_parameters(); + await base.Parameter_collection_Count_with_column_predicate_with_default_mode_EF_MultipleParameters(mode); AssertSql( """ @@ -469,9 +518,9 @@ SELECT COUNT(*) """); } - public override async Task Parameter_collection_Contains_with_default_multiple_parameters() + public override async Task Parameter_collection_Contains_with_default_mode_EF_MultipleParameters(ParameterizedCollectionMode mode) { - await base.Parameter_collection_Contains_with_default_multiple_parameters(); + await base.Parameter_collection_Contains_with_default_mode_EF_MultipleParameters(mode); AssertSql( """ From 717cc6de57017c9a82a7d64b711f142671dc9913 Mon Sep 17 00:00:00 2001 From: Jiri Cincura Date: Tue, 15 Jul 2025 11:28:32 +0200 Subject: [PATCH 03/10] Feedback --- .../RelationalQueryableMethodTranslatingExpressionVisitor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index a328de74fbc..b456fa04896 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -244,8 +244,8 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp && methodCallExpression.Arguments[0] is ParameterQueryRootExpression parameterSource && TranslateExpression(methodCallExpression.Arguments[1]) is SqlExpression item && _sqlTranslator.Visit(parameterSource.QueryParameterExpression) is SqlParameterExpression sqlParameterExpression - && (parameterSource.QueryParameterExpression.ParameterExpressionMode is not ParameterExpressionMode.Parameter - && parameterSource.QueryParameterExpression.ParameterExpressionMode is not ParameterExpressionMode.MultipleParameters)) + && (parameterSource.QueryParameterExpression.ParameterExpressionMode is ParameterExpressionMode.Constants + or null)) { var inExpression = _sqlExpressionFactory.In(item, sqlParameterExpression); var selectExpression = new SelectExpression(inExpression, _sqlAliasManager); From 1b732793d1e17a14f184b20be4b9357561ec3e93 Mon Sep 17 00:00:00 2001 From: Jiri Cincura Date: Tue, 15 Jul 2025 11:35:56 +0200 Subject: [PATCH 04/10] Feedback --- .../Query/SqlNullabilityProcessor.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs index c0ec7c9cbfb..7d201677632 100644 --- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs +++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs @@ -826,6 +826,14 @@ InExpression ProcessInExpressionValues( var useParameter = valuesParameter.ParameterExpressionMode is ParameterExpressionMode.Parameter || (ParameterizedCollectionMode is ParameterizedCollectionMode.Parameter && valuesParameter.ParameterExpressionMode is null); + var parameterMode = valuesParameter.ParameterExpressionMode + ?? ParameterizedCollectionMode switch + { + ParameterizedCollectionMode.MultipleParameters => ParameterExpressionMode.MultipleParameters, + ParameterizedCollectionMode.Constants => ParameterExpressionMode.Constants, + ParameterizedCollectionMode.Parameter => ParameterExpressionMode.Parameter, + _ => throw new UnreachableException() + }; var expandedParameters = _collectionParameterExpansionMap.GetOrAddNew(valuesParameter); var expandedParametersCounter = 0; for (var i = 0; i < values.Count; i++) @@ -836,11 +844,11 @@ InExpression ProcessInExpressionValues( continue; } - switch (useMultipleParameters, useConstants, useParameter) + switch (parameterMode) { - case (true, false, false): + case ParameterExpressionMode.MultipleParameters: // see #36311 for more info - case (false, false, true): + case ParameterExpressionMode.Parameter: { // Create parameter for value if we didn't create it yet, // otherwise reuse it. @@ -858,7 +866,7 @@ InExpression ProcessInExpressionValues( break; } - case (false, true, false): + case ParameterExpressionMode.Constants: { processedValues.Add(_sqlExpressionFactory.Constant(values[i], values[i]?.GetType() ?? typeof(object), sensitive: true, elementTypeMapping)); From 2f8aba502f895bd7191cfe120ed3018f857b3d06 Mon Sep 17 00:00:00 2001 From: Jiri Cincura Date: Tue, 15 Jul 2025 11:43:37 +0200 Subject: [PATCH 05/10] Feedback --- .../Internal/ExpressionTreeFuncletizer.cs | 41 ++++++++----------- ...yableMethodNormalizingExpressionVisitor.cs | 20 +++++---- 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs b/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs index 0618db01d2a..616914ed9ac 100644 --- a/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs +++ b/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs @@ -952,35 +952,15 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCall) case nameof(EF.Parameter): { - var argument = Visit(methodCall.Arguments[0], out var argumentState); - - if (!argumentState.IsEvaluatable) - { - throw new InvalidOperationException(CoreStrings.EFMethodWithNonEvaluatableArgument("EF.Parameter")); - } - - argumentState = argumentState with { StateType = StateType.EvaluatableWithCapturedVariable }; - var evaluatedArgument = ProcessEvaluatableRoot(argument, ref argumentState, forceEvaluation: true); - _state = argumentState; - return Call(method, evaluatedArgument); + return HandleParameter(methodCall, "EF.Parameter"); } } } // EF.MultipleParameters is defined in Relational, hence the hardcoded values here. - if (method.DeclaringType?.FullName == "Microsoft.EntityFrameworkCore.EFExtensions" && method.Name == "MultipleParameters") + if (method is { Name: "MultipleParameters", DeclaringType.FullName: "Microsoft.EntityFrameworkCore.EFExtensions" }) { - var argument = Visit(methodCall.Arguments[0], out var argumentState); - - if (!argumentState.IsEvaluatable) - { - throw new InvalidOperationException(CoreStrings.EFMethodWithNonEvaluatableArgument("EF.MultipleParameters")); - } - - argumentState = argumentState with { StateType = StateType.EvaluatableWithCapturedVariable }; - var evaluatedArgument = ProcessEvaluatableRoot(argument, ref argumentState, forceEvaluation: true); - _state = argumentState; - return Call(method, evaluatedArgument); + return HandleParameter(methodCall, "EF.MultipleParameters"); } // .NET 10 made changes to overload resolution to prefer Span-based overloads when those exist ("first-class spans"). @@ -1132,6 +1112,21 @@ static bool TryUnwrapSpanImplicitCast(Expression expression, [NotNullWhen(true)] } return methodCall.Update(@object, ((IReadOnlyList?)arguments) ?? methodCall.Arguments); + + Expression HandleParameter(MethodCallExpression methodCall, string methodName) + { + var argument = Visit(methodCall.Arguments[0], out var argumentState); + + if (!argumentState.IsEvaluatable) + { + throw new InvalidOperationException(CoreStrings.EFMethodWithNonEvaluatableArgument(methodName)); + } + + argumentState = argumentState with { StateType = StateType.EvaluatableWithCapturedVariable }; + var evaluatedArgument = ProcessEvaluatableRoot(argument, ref argumentState, forceEvaluation: true); + _state = argumentState; + return Call(methodCall.Method, evaluatedArgument); + } } /// diff --git a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs index a1d094671aa..28b5646bc7f 100644 --- a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs @@ -134,21 +134,15 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp case nameof(EF.Parameter): { - var queryParameter = (QueryParameterExpression)Visit(methodCallExpression.Arguments[0]); - return new QueryParameterExpression( - queryParameter.Name, queryParameter.Type, parameterExpressionMode: ParameterExpressionMode.Parameter, - queryParameter.IsNonNullableReferenceType); + return HandleParameter(methodCallExpression, ParameterExpressionMode.Parameter); } } } // EF.MultipleParameters is defined in Relational, hence the hardcoded values here. - if (method.DeclaringType?.FullName == "Microsoft.EntityFrameworkCore.EFExtensions" && method.Name == "MultipleParameters") + if (method is { Name: "MultipleParameters", DeclaringType.FullName: "Microsoft.EntityFrameworkCore.EFExtensions" }) { - var queryParameter = (QueryParameterExpression)Visit(methodCallExpression.Arguments[0]); - return new QueryParameterExpression( - queryParameter.Name, queryParameter.Type, parameterExpressionMode: ParameterExpressionMode.MultipleParameters, - queryParameter.IsNonNullableReferenceType); + return HandleParameter(methodCallExpression, ParameterExpressionMode.MultipleParameters); } // Normalize list[x] to list.ElementAt(x) @@ -246,6 +240,14 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp } return visitedExpression; + + Expression HandleParameter(MethodCallExpression methodCallExpression, ParameterExpressionMode parameterExpressionMode) + { + var queryParameter = (QueryParameterExpression)Visit(methodCallExpression.Arguments[0]); + return new QueryParameterExpression( + queryParameter.Name, queryParameter.Type, parameterExpressionMode, + queryParameter.IsNonNullableReferenceType); + } } private static void VerifyReturnType(Expression expression, ParameterExpression lambdaParameter) From e622c11bbc7d4efa3ff40795e1f88796a25ed5c2 Mon Sep 17 00:00:00 2001 From: Jiri Cincura Date: Tue, 15 Jul 2025 11:45:23 +0200 Subject: [PATCH 06/10] Feedback --- .../Query/SqlNullabilityProcessor.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs index 7d201677632..67e613d4f91 100644 --- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs +++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs @@ -127,10 +127,16 @@ protected override Expression VisitExtension(Expression node) var processedValues = new List(); - switch (ParameterizedCollectionMode, valuesParameter.ParameterExpressionMode) + var parameterMode = valuesParameter.ParameterExpressionMode + ?? ParameterizedCollectionMode switch + { + ParameterizedCollectionMode.MultipleParameters => ParameterExpressionMode.MultipleParameters, + ParameterizedCollectionMode.Constants => ParameterExpressionMode.Constants, + _ => throw new UnreachableException() + }; + switch (parameterMode) { - case (ParameterizedCollectionMode.MultipleParameters, null): - case (_, ParameterExpressionMode.MultipleParameters): + case ParameterExpressionMode.MultipleParameters: { var expandedParameters = _collectionParameterExpansionMap.GetOrAddNew(valuesParameter); for (var i = 0; i < values.Count; i++) @@ -156,8 +162,7 @@ protected override Expression VisitExtension(Expression node) break; } - case (ParameterizedCollectionMode.Constants, null): - case (_, ParameterExpressionMode.Constants): + case ParameterExpressionMode.Constants: { foreach (var value in values) { From c52ddd293f404c91eacdf4d1e59569220273b1bf Mon Sep 17 00:00:00 2001 From: Jiri Cincura Date: Tue, 15 Jul 2025 11:51:57 +0200 Subject: [PATCH 07/10] Feedback --- .../ParameterizedCollectionMode.cs | 30 ++++++------ ...yableMethodTranslatingExpressionVisitor.cs | 47 ++++++++++--------- .../Query/SqlNullabilityProcessor.cs | 9 ---- 3 files changed, 41 insertions(+), 45 deletions(-) diff --git a/src/EFCore.Relational/ParameterizedCollectionMode.cs b/src/EFCore.Relational/ParameterizedCollectionMode.cs index 89f48bea90a..c3e924f5162 100644 --- a/src/EFCore.Relational/ParameterizedCollectionMode.cs +++ b/src/EFCore.Relational/ParameterizedCollectionMode.cs @@ -8,6 +8,19 @@ namespace Microsoft.EntityFrameworkCore; /// public enum ParameterizedCollectionMode { + /// + /// Instructs EF to translate the collection to a set of parameters: + /// WHERE [x].[Id] IN (@ids1, @ids2, @ids3). + /// + /// + /// + /// Note that it's possible to cause EF to translate a specific collection in a specific query to parameter by wrapping the + /// parameterized collection in : Where(x => EF.MultipleParameters(ids).Contains(x.Id). This overrides + /// the default. + /// + /// + MultipleParameters = 0, + /// /// Instructs EF to translate the collection to a set of constants: /// WHERE [x].[Id] IN (1, 2, 3). @@ -23,7 +36,7 @@ public enum ParameterizedCollectionMode /// the default. /// /// - Constants, + Constants = 1, /// /// Instructs EF to translate the collection to a single array-like parameter: @@ -39,18 +52,5 @@ public enum ParameterizedCollectionMode /// the default. /// /// - Parameter, - - /// - /// Instructs EF to translate the collection to a set of parameters: - /// WHERE [x].[Id] IN (@ids1, @ids2, @ids3). - /// - /// - /// - /// Note that it's possible to cause EF to translate a specific collection in a specific query to parameter by wrapping the - /// parameterized collection in : Where(x => EF.MultipleParameters(ids).Contains(x.Id). This overrides - /// the default. - /// - /// - MultipleParameters, + Parameter = 2, } diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index b456fa04896..e01cbd12a92 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -299,27 +299,32 @@ JsonScalarExpression jsonScalar var tableAlias = _sqlAliasManager.GenerateTableAlias(sqlParameterExpression.Name.TrimStart('_')); - var constants = queryParameter.ParameterExpressionMode is ParameterExpressionMode.Constants - || (_parameterizedCollectionMode is ParameterizedCollectionMode.Constants - && queryParameter.ParameterExpressionMode is null); - var multipleParameters = queryParameter.ParameterExpressionMode is ParameterExpressionMode.MultipleParameters - || (_parameterizedCollectionMode is ParameterizedCollectionMode.MultipleParameters - && queryParameter.ParameterExpressionMode is null); - if (constants || multipleParameters) - { - var valuesExpression = new ValuesExpression( - tableAlias, - sqlParameterExpression, - [ValuesOrderingColumnName, ValuesValueColumnName]); - return CreateShapedQueryExpressionForValuesExpression( - valuesExpression, - tableAlias, - parameterQueryRootExpression.ElementType, - sqlParameterExpression.TypeMapping, - sqlParameterExpression.IsNullable); - } - - return TranslatePrimitiveCollection(sqlParameterExpression, property: null, tableAlias); + var parameterMode = queryParameter.ParameterExpressionMode + ?? _parameterizedCollectionMode switch + { + ParameterizedCollectionMode.MultipleParameters => ParameterExpressionMode.MultipleParameters, + ParameterizedCollectionMode.Constants => ParameterExpressionMode.Constants, + ParameterizedCollectionMode.Parameter => ParameterExpressionMode.Parameter, + _ => throw new UnreachableException() + }; + return parameterMode switch + { + ParameterExpressionMode.Constants or ParameterExpressionMode.MultipleParameters + => CreateShapedQueryExpressionForValuesExpression( + new ValuesExpression( + tableAlias, + sqlParameterExpression, + [ValuesOrderingColumnName, ValuesValueColumnName]), + tableAlias, + parameterQueryRootExpression.ElementType, + sqlParameterExpression.TypeMapping, + sqlParameterExpression.IsNullable), + + ParameterExpressionMode.Parameter + => TranslatePrimitiveCollection(sqlParameterExpression, property: null, tableAlias), + + _ => throw new UnreachableException() + }; } /// diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs index 67e613d4f91..9d68e35fba1 100644 --- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs +++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs @@ -822,15 +822,6 @@ InExpression ProcessInExpressionValues( processedValues = []; - var useMultipleParameters = valuesParameter.ParameterExpressionMode is ParameterExpressionMode.MultipleParameters - || (ParameterizedCollectionMode is ParameterizedCollectionMode.MultipleParameters - && valuesParameter.ParameterExpressionMode is null); - var useConstants = valuesParameter.ParameterExpressionMode is ParameterExpressionMode.Constants - || (ParameterizedCollectionMode is ParameterizedCollectionMode.Constants - && valuesParameter.ParameterExpressionMode is null); - var useParameter = valuesParameter.ParameterExpressionMode is ParameterExpressionMode.Parameter - || (ParameterizedCollectionMode is ParameterizedCollectionMode.Parameter - && valuesParameter.ParameterExpressionMode is null); var parameterMode = valuesParameter.ParameterExpressionMode ?? ParameterizedCollectionMode switch { From ad85831c6de43f0ebe589089e84443d1f62e705f Mon Sep 17 00:00:00 2001 From: Jiri Cincura Date: Tue, 15 Jul 2025 12:45:44 +0200 Subject: [PATCH 08/10] Feedback --- .../RelationalDbContextOptionsBuilder.cs | 8 ++--- .../RelationalOptionsExtension.cs | 8 ++--- .../Query/Internal/RelationalCommandCache.cs | 2 +- .../Internal/RelationalParameterProcessor.cs | 2 +- ...nalParameterBasedSqlProcessorParameters.cs | 4 +-- ...yableMethodTranslatingExpressionVisitor.cs | 17 +++------- ...alShapedQueryCompilingExpressionVisitor.cs | 4 +-- ...lationalSqlTranslatingExpressionVisitor.cs | 4 +-- .../SqlExpressions/SqlParameterExpression.cs | 12 +++---- .../Query/SqlNullabilityProcessor.cs | 32 ++++++------------- .../SqlServerSqlNullabilityProcessor.cs | 8 ++--- .../ParameterTranslationMode.cs} | 6 ++-- .../Internal/ExpressionTreeFuncletizer.cs | 2 +- ...yableMethodNormalizingExpressionVisitor.cs | 10 +++--- src/EFCore/Query/ParameterExpressionMode.cs | 25 --------------- src/EFCore/Query/QueryParameterExpression.cs | 16 +++++----- ...HocMiscellaneousQueryRelationalTestBase.cs | 6 ++-- ...itiveCollectionsQueryRelationalTestBase.cs | 22 +++++++------ .../AdHocMiscellaneousQuerySqlServerTest.cs | 2 +- ...dPrimitiveCollectionsQuerySqlServerTest.cs | 30 ++++++++--------- .../AdHocMiscellaneousQuerySqliteTest.cs | 2 +- ...aredPrimitiveCollectionsQuerySqliteTest.cs | 30 ++++++++--------- 22 files changed, 102 insertions(+), 150 deletions(-) rename src/{EFCore.Relational/ParameterizedCollectionMode.cs => EFCore/ParameterTranslationMode.cs} (90%) delete mode 100644 src/EFCore/Query/ParameterExpressionMode.cs diff --git a/src/EFCore.Relational/Infrastructure/RelationalDbContextOptionsBuilder.cs b/src/EFCore.Relational/Infrastructure/RelationalDbContextOptionsBuilder.cs index 269a68bfb0a..3750bfd03aa 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalDbContextOptionsBuilder.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalDbContextOptionsBuilder.cs @@ -179,7 +179,7 @@ public virtual TBuilder ExecutionStrategy( /// [Obsolete("Use UseParameterizedCollectionMode instead.")] public virtual TBuilder TranslateParameterizedCollectionsToConstants() - => UseParameterizedCollectionMode(ParameterizedCollectionMode.Constants); + => UseParameterizedCollectionMode(ParameterTranslationMode.Constant); /// /// Configures the context to translate parameterized collections to a single array-like parameter. @@ -202,13 +202,13 @@ public virtual TBuilder TranslateParameterizedCollectionsToConstants() /// [Obsolete("Use UseParameterizedCollectionMode instead.")] public virtual TBuilder TranslateParameterizedCollectionsToParameters() - => UseParameterizedCollectionMode(ParameterizedCollectionMode.Parameter); + => UseParameterizedCollectionMode(ParameterTranslationMode.Parameter); /// - /// Configures the to use when translating parameterized collections. + /// Configures mode to use when translating parameterized collections. /// /// The same builder instance so that multiple calls can be chained. - public virtual TBuilder UseParameterizedCollectionMode(ParameterizedCollectionMode parameterizedCollectionMode) + public virtual TBuilder UseParameterizedCollectionMode(ParameterTranslationMode parameterizedCollectionMode) => WithOption(e => (TExtension)e.WithUseParameterizedCollectionMode(parameterizedCollectionMode)); /// diff --git a/src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs b/src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs index e2cc09b80c8..a052b74c1eb 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs @@ -36,7 +36,7 @@ public abstract class RelationalOptionsExtension : IDbContextOptionsExtension private string? _migrationsHistoryTableName; private string? _migrationsHistoryTableSchema; private Func? _executionStrategyFactory; - private ParameterizedCollectionMode? _parameterizedCollectionMode; + private ParameterTranslationMode? _parameterizedCollectionMode; /// /// Creates a new set of options with everything set to default values. @@ -386,8 +386,8 @@ public virtual RelationalOptionsExtension WithExecutionStrategyFactory( /// /// Configured translation mode for parameterized collections. /// - public virtual ParameterizedCollectionMode ParameterizedCollectionMode - => _parameterizedCollectionMode ?? ParameterizedCollectionMode.MultipleParameters; + public virtual ParameterTranslationMode ParameterizedCollectionMode + => _parameterizedCollectionMode ?? ParameterTranslationMode.MultipleParameters; /// /// Creates a new instance with all options the same as for this instance, but with the given option changed. @@ -395,7 +395,7 @@ public virtual ParameterizedCollectionMode ParameterizedCollectionMode /// /// The option to change. public virtual RelationalOptionsExtension WithUseParameterizedCollectionMode( - ParameterizedCollectionMode parameterizedCollectionMode) + ParameterTranslationMode parameterizedCollectionMode) { var clone = Clone(); diff --git a/src/EFCore.Relational/Query/Internal/RelationalCommandCache.cs b/src/EFCore.Relational/Query/Internal/RelationalCommandCache.cs index 4d19a2b3eaa..283920a033a 100644 --- a/src/EFCore.Relational/Query/Internal/RelationalCommandCache.cs +++ b/src/EFCore.Relational/Query/Internal/RelationalCommandCache.cs @@ -35,7 +35,7 @@ public RelationalCommandCache( IRelationalParameterBasedSqlProcessorFactory relationalParameterBasedSqlProcessorFactory, Expression queryExpression, bool useRelationalNulls, - ParameterizedCollectionMode parameterizedCollectionMode) + ParameterTranslationMode parameterizedCollectionMode) { _memoryCache = memoryCache; _querySqlGeneratorFactory = querySqlGeneratorFactory; diff --git a/src/EFCore.Relational/Query/Internal/RelationalParameterProcessor.cs b/src/EFCore.Relational/Query/Internal/RelationalParameterProcessor.cs index 48bf0ae6bfe..f537a04f3be 100644 --- a/src/EFCore.Relational/Query/Internal/RelationalParameterProcessor.cs +++ b/src/EFCore.Relational/Query/Internal/RelationalParameterProcessor.cs @@ -128,7 +128,7 @@ private SqlParameterExpression VisitSqlParameter(SqlParameterExpression paramete uniquifiedName, parameter.Type, parameter.IsNullable, - parameter.ParameterExpressionMode, + parameter.ParameterTranslationMode, parameter.TypeMapping); return _sqlParameters[newParameter.InvariantName] = newParameter; diff --git a/src/EFCore.Relational/Query/RelationalParameterBasedSqlProcessorParameters.cs b/src/EFCore.Relational/Query/RelationalParameterBasedSqlProcessorParameters.cs index 415a8db6e54..eb5f9ae57bf 100644 --- a/src/EFCore.Relational/Query/RelationalParameterBasedSqlProcessorParameters.cs +++ b/src/EFCore.Relational/Query/RelationalParameterBasedSqlProcessorParameters.cs @@ -16,7 +16,7 @@ public sealed record RelationalParameterBasedSqlProcessorParameters /// /// Which parametrized collection translation mode should be used. /// - public ParameterizedCollectionMode ParameterizedCollectionMode { get; init; } + public ParameterTranslationMode ParameterizedCollectionMode { get; init; } /// /// Creates a new instance of . @@ -24,7 +24,7 @@ public sealed record RelationalParameterBasedSqlProcessorParameters /// A value indicating if relational nulls should be used. /// Which translation mode should be used. [EntityFrameworkInternal] - public RelationalParameterBasedSqlProcessorParameters(bool useRelationalNulls, ParameterizedCollectionMode parameterizedCollectionMode) + public RelationalParameterBasedSqlProcessorParameters(bool useRelationalNulls, ParameterTranslationMode parameterizedCollectionMode) { UseRelationalNulls = useRelationalNulls; ParameterizedCollectionMode = parameterizedCollectionMode; diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index e01cbd12a92..73eb34ccf7a 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -21,7 +21,7 @@ public partial class RelationalQueryableMethodTranslatingExpressionVisitor : Que private readonly IRelationalTypeMappingSource _typeMappingSource; private readonly ISqlExpressionFactory _sqlExpressionFactory; private readonly bool _subquery; - private readonly ParameterizedCollectionMode _parameterizedCollectionMode; + private readonly ParameterTranslationMode _parameterizedCollectionMode; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -244,7 +244,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp && methodCallExpression.Arguments[0] is ParameterQueryRootExpression parameterSource && TranslateExpression(methodCallExpression.Arguments[1]) is SqlExpression item && _sqlTranslator.Visit(parameterSource.QueryParameterExpression) is SqlParameterExpression sqlParameterExpression - && (parameterSource.QueryParameterExpression.ParameterExpressionMode is ParameterExpressionMode.Constants + && (parameterSource.QueryParameterExpression.ParameterTranslationMode is ParameterTranslationMode.Constant or null)) { var inExpression = _sqlExpressionFactory.In(item, sqlParameterExpression); @@ -299,17 +299,10 @@ JsonScalarExpression jsonScalar var tableAlias = _sqlAliasManager.GenerateTableAlias(sqlParameterExpression.Name.TrimStart('_')); - var parameterMode = queryParameter.ParameterExpressionMode - ?? _parameterizedCollectionMode switch - { - ParameterizedCollectionMode.MultipleParameters => ParameterExpressionMode.MultipleParameters, - ParameterizedCollectionMode.Constants => ParameterExpressionMode.Constants, - ParameterizedCollectionMode.Parameter => ParameterExpressionMode.Parameter, - _ => throw new UnreachableException() - }; + var parameterMode = queryParameter.ParameterTranslationMode ?? _parameterizedCollectionMode; return parameterMode switch { - ParameterExpressionMode.Constants or ParameterExpressionMode.MultipleParameters + ParameterTranslationMode.Constant or ParameterTranslationMode.MultipleParameters => CreateShapedQueryExpressionForValuesExpression( new ValuesExpression( tableAlias, @@ -320,7 +313,7 @@ ParameterExpressionMode.Constants or ParameterExpressionMode.MultipleParameters sqlParameterExpression.TypeMapping, sqlParameterExpression.IsNullable), - ParameterExpressionMode.Parameter + ParameterTranslationMode.Parameter => TranslatePrimitiveCollection(sqlParameterExpression, property: null, tableAlias), _ => throw new UnreachableException() diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs index aec6e3cc14d..1eadeaf41ad 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs @@ -19,7 +19,7 @@ public partial class RelationalShapedQueryCompilingExpressionVisitor : ShapedQue private readonly bool _threadSafetyChecksEnabled; private readonly bool _detailedErrorsEnabled; private readonly bool _useRelationalNulls; - private readonly ParameterizedCollectionMode _parameterizedCollectionMode; + private readonly ParameterTranslationMode _parameterizedCollectionMode; private readonly bool _isPrecompiling; private readonly RelationalParameterBasedSqlProcessor _relationalParameterBasedSqlProcessor; @@ -751,7 +751,7 @@ Expression> Generate _relationalDependenciesRelationalParameterBasedSqlProcessorFactoryProperty), Constant(queryExpression), Constant(_useRelationalNulls), - Constant(_parameterizedCollectionMode, typeof(ParameterizedCollectionMode))), + Constant(_parameterizedCollectionMode, typeof(ParameterTranslationMode))), contextParameter); } } diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs index 0e9c511f44d..5072dc0b55e 100644 --- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs @@ -516,7 +516,7 @@ protected override Expression VisitExtension(Expression extensionExpression) name: queryParameter.Name, queryParameter.Type, nullable: false, - queryParameter.ParameterExpressionMode, + queryParameter.ParameterTranslationMode, typeMapping: null); } @@ -525,7 +525,7 @@ protected override Expression VisitExtension(Expression extensionExpression) name: queryParameter.Name, queryParameter.Type, queryParameter.Type.IsNullableType(), - queryParameter.ParameterExpressionMode, + queryParameter.ParameterTranslationMode, typeMapping: null); case StructuralTypeShaperExpression shaper: diff --git a/src/EFCore.Relational/Query/SqlExpressions/SqlParameterExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SqlParameterExpression.cs index 53aab11f607..0c32405bca7 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SqlParameterExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SqlParameterExpression.cs @@ -17,7 +17,7 @@ public sealed class SqlParameterExpression : SqlExpression /// The of the expression. /// The associated with the expression. public SqlParameterExpression(string name, Type type, RelationalTypeMapping? typeMapping) - : this(invariantName: name, name: name, type.UnwrapNullableType(), type.IsNullableType(), parameterExpressionMode: null, typeMapping) + : this(invariantName: name, name: name, type.UnwrapNullableType(), type.IsNullableType(), parameterTranslationMode: null, typeMapping) { } @@ -31,21 +31,21 @@ public SqlParameterExpression(string name, Type type, RelationalTypeMapping? typ /// /// The of the expression. /// Whether this parameter can have null values. - /// How the parameter should be handled. + /// How the parameter should be handled. /// The associated with the expression. public SqlParameterExpression( string invariantName, string name, Type type, bool nullable, - ParameterExpressionMode? parameterExpressionMode, + ParameterTranslationMode? parameterTranslationMode, RelationalTypeMapping? typeMapping) : base(type.UnwrapNullableType(), typeMapping) { InvariantName = invariantName; Name = name; IsNullable = nullable; - ParameterExpressionMode = parameterExpressionMode; + ParameterTranslationMode = parameterTranslationMode; } /// @@ -67,7 +67,7 @@ public SqlParameterExpression( /// /// How the parameter should be handled. /// - public ParameterExpressionMode? ParameterExpressionMode { get; } + public ParameterTranslationMode? ParameterTranslationMode { get; } /// /// Applies supplied type mapping to this expression. @@ -75,7 +75,7 @@ public SqlParameterExpression( /// A relational type mapping to apply. /// A new expression which has supplied type mapping. public SqlExpression ApplyTypeMapping(RelationalTypeMapping? typeMapping) - => new SqlParameterExpression(InvariantName, Name, Type, IsNullable, ParameterExpressionMode, typeMapping); + => new SqlParameterExpression(InvariantName, Name, Type, IsNullable, ParameterTranslationMode, typeMapping); /// protected override Expression VisitChildren(ExpressionVisitor visitor) diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs index 9d68e35fba1..1c6e62e0b79 100644 --- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs +++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs @@ -4,7 +4,6 @@ using System.Collections; using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Query.SqlExpressions; -using Microsoft.EntityFrameworkCore.Storage; namespace Microsoft.EntityFrameworkCore.Query; @@ -62,7 +61,7 @@ public SqlNullabilityProcessor( /// /// A value indicating what translation mode to use. /// - public virtual ParameterizedCollectionMode ParameterizedCollectionMode { get; } + public virtual ParameterTranslationMode ParameterizedCollectionMode { get; } /// /// Dictionary of current parameter values in use. @@ -127,16 +126,10 @@ protected override Expression VisitExtension(Expression node) var processedValues = new List(); - var parameterMode = valuesParameter.ParameterExpressionMode - ?? ParameterizedCollectionMode switch - { - ParameterizedCollectionMode.MultipleParameters => ParameterExpressionMode.MultipleParameters, - ParameterizedCollectionMode.Constants => ParameterExpressionMode.Constants, - _ => throw new UnreachableException() - }; + var parameterMode = valuesParameter.ParameterTranslationMode ?? ParameterizedCollectionMode; switch (parameterMode) { - case ParameterExpressionMode.MultipleParameters: + case ParameterTranslationMode.MultipleParameters: { var expandedParameters = _collectionParameterExpansionMap.GetOrAddNew(valuesParameter); for (var i = 0; i < values.Count; i++) @@ -162,7 +155,7 @@ protected override Expression VisitExtension(Expression node) break; } - case ParameterExpressionMode.Constants: + case ParameterTranslationMode.Constant: { foreach (var value in values) { @@ -822,14 +815,7 @@ InExpression ProcessInExpressionValues( processedValues = []; - var parameterMode = valuesParameter.ParameterExpressionMode - ?? ParameterizedCollectionMode switch - { - ParameterizedCollectionMode.MultipleParameters => ParameterExpressionMode.MultipleParameters, - ParameterizedCollectionMode.Constants => ParameterExpressionMode.Constants, - ParameterizedCollectionMode.Parameter => ParameterExpressionMode.Parameter, - _ => throw new UnreachableException() - }; + var parameterMode = valuesParameter.ParameterTranslationMode ?? ParameterizedCollectionMode; var expandedParameters = _collectionParameterExpansionMap.GetOrAddNew(valuesParameter); var expandedParametersCounter = 0; for (var i = 0; i < values.Count; i++) @@ -842,9 +828,9 @@ InExpression ProcessInExpressionValues( switch (parameterMode) { - case ParameterExpressionMode.MultipleParameters: + case ParameterTranslationMode.MultipleParameters: // see #36311 for more info - case ParameterExpressionMode.Parameter: + case ParameterTranslationMode.Parameter: { // Create parameter for value if we didn't create it yet, // otherwise reuse it. @@ -862,7 +848,7 @@ InExpression ProcessInExpressionValues( break; } - case ParameterExpressionMode.Constants: + case ParameterTranslationMode.Constant: { processedValues.Add(_sqlExpressionFactory.Constant(values[i], values[i]?.GetType() ?? typeof(object), sensitive: true, elementTypeMapping)); @@ -1424,7 +1410,7 @@ protected virtual SqlExpression VisitSqlParameter( nullable = false; - if (sqlParameterExpression.ParameterExpressionMode is ParameterExpressionMode.Constants) + if (sqlParameterExpression.ParameterTranslationMode is ParameterTranslationMode.Constant) { var parameters = ParametersFacade.GetParametersAndDisableSqlCaching(); diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs index c08b2902da3..35375ed6b11 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs @@ -190,9 +190,7 @@ protected override Expression VisitExtension(Expression node) switch (node) { case ValuesExpression { ValuesParameter: SqlParameterExpression valuesParameter } valuesExpression - when valuesParameter.ParameterExpressionMode is ParameterExpressionMode.MultipleParameters - || (ParameterizedCollectionMode is ParameterizedCollectionMode.MultipleParameters - && valuesParameter.ParameterExpressionMode is null): + when (valuesParameter.ParameterTranslationMode ?? ParameterizedCollectionMode) is ParameterTranslationMode.MultipleParameters: { Check.DebugAssert(valuesParameter.TypeMapping is not null); Check.DebugAssert(valuesParameter.TypeMapping.ElementTypeMapping is not null); @@ -238,9 +236,7 @@ protected override SqlExpression VisitIn(InExpression inExpression, bool allowOp switch (inExpression.ValuesParameter) { case SqlParameterExpression valuesParameter - when valuesParameter.ParameterExpressionMode is ParameterExpressionMode.MultipleParameters - || (ParameterizedCollectionMode is ParameterizedCollectionMode.MultipleParameters - && valuesParameter.ParameterExpressionMode is null): + when (valuesParameter.ParameterTranslationMode ?? ParameterizedCollectionMode) is ParameterTranslationMode.MultipleParameters: { Check.DebugAssert(valuesParameter.TypeMapping is not null); Check.DebugAssert(valuesParameter.TypeMapping.ElementTypeMapping is not null); diff --git a/src/EFCore.Relational/ParameterizedCollectionMode.cs b/src/EFCore/ParameterTranslationMode.cs similarity index 90% rename from src/EFCore.Relational/ParameterizedCollectionMode.cs rename to src/EFCore/ParameterTranslationMode.cs index c3e924f5162..964a53688e9 100644 --- a/src/EFCore.Relational/ParameterizedCollectionMode.cs +++ b/src/EFCore/ParameterTranslationMode.cs @@ -6,7 +6,7 @@ namespace Microsoft.EntityFrameworkCore; /// /// Indicates how parameterized collections are translated into SQL. /// -public enum ParameterizedCollectionMode +public enum ParameterTranslationMode { /// /// Instructs EF to translate the collection to a set of parameters: @@ -15,7 +15,7 @@ public enum ParameterizedCollectionMode /// /// /// Note that it's possible to cause EF to translate a specific collection in a specific query to parameter by wrapping the - /// parameterized collection in : Where(x => EF.MultipleParameters(ids).Contains(x.Id). This overrides + /// parameterized collection in EF.MultipleParameters: Where(x => EF.MultipleParameters(ids).Contains(x.Id). This overrides /// the default. /// /// @@ -36,7 +36,7 @@ public enum ParameterizedCollectionMode /// the default. /// /// - Constants = 1, + Constant = 1, /// /// Instructs EF to translate the collection to a single array-like parameter: diff --git a/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs b/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs index 616914ed9ac..d4250c25686 100644 --- a/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs +++ b/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs @@ -1988,7 +1988,7 @@ private static StateType CombineStateTypes(StateType stateType1, StateType state return _parameterizedValues[evaluatableRoot] = new QueryParameterExpression( parameterName, evaluatableRoot.Type, - parameterExpressionMode: null, + parameterTranslationMode: null, isNonNullableReferenceType); } diff --git a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs index 28b5646bc7f..d2fd64b13f6 100644 --- a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs @@ -128,13 +128,13 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp var queryParameter = (QueryParameterExpression)Visit(methodCallExpression.Arguments[0]); return new QueryParameterExpression( - queryParameter.Name, queryParameter.Type, parameterExpressionMode: ParameterExpressionMode.Constants, + queryParameter.Name, queryParameter.Type, parameterTranslationMode: ParameterTranslationMode.Constant, queryParameter.IsNonNullableReferenceType); } case nameof(EF.Parameter): { - return HandleParameter(methodCallExpression, ParameterExpressionMode.Parameter); + return HandleParameter(methodCallExpression, ParameterTranslationMode.Parameter); } } } @@ -142,7 +142,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp // EF.MultipleParameters is defined in Relational, hence the hardcoded values here. if (method is { Name: "MultipleParameters", DeclaringType.FullName: "Microsoft.EntityFrameworkCore.EFExtensions" }) { - return HandleParameter(methodCallExpression, ParameterExpressionMode.MultipleParameters); + return HandleParameter(methodCallExpression, ParameterTranslationMode.MultipleParameters); } // Normalize list[x] to list.ElementAt(x) @@ -241,11 +241,11 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp return visitedExpression; - Expression HandleParameter(MethodCallExpression methodCallExpression, ParameterExpressionMode parameterExpressionMode) + Expression HandleParameter(MethodCallExpression methodCallExpression, ParameterTranslationMode parameterTranslationMode) { var queryParameter = (QueryParameterExpression)Visit(methodCallExpression.Arguments[0]); return new QueryParameterExpression( - queryParameter.Name, queryParameter.Type, parameterExpressionMode, + queryParameter.Name, queryParameter.Type, parameterTranslationMode, queryParameter.IsNonNullableReferenceType); } } diff --git a/src/EFCore/Query/ParameterExpressionMode.cs b/src/EFCore/Query/ParameterExpressionMode.cs deleted file mode 100644 index 6ebf027270e..00000000000 --- a/src/EFCore/Query/ParameterExpressionMode.cs +++ /dev/null @@ -1,25 +0,0 @@ -// 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.Query; - -/// -/// Describes how the parameter expression in a query should be handled. -/// -public enum ParameterExpressionMode -{ - /// - /// Handle as constants. - /// - Constants, - - /// - /// Handle as parameter. - /// - Parameter, - - /// - /// Handle as multiple parameters. - /// - MultipleParameters, -} diff --git a/src/EFCore/Query/QueryParameterExpression.cs b/src/EFCore/Query/QueryParameterExpression.cs index 7e283b2c3d4..a15248cf241 100644 --- a/src/EFCore/Query/QueryParameterExpression.cs +++ b/src/EFCore/Query/QueryParameterExpression.cs @@ -17,15 +17,15 @@ public class QueryParameterExpression : Expression, IPrintableExpression /// Creates a new instance of the class with associated query provider. /// public QueryParameterExpression(string name, Type type) - : this(name, type, parameterExpressionMode: null, isNonNullableReferenceType: false) + : this(name, type, parameterTranslationMode: null, isNonNullableReferenceType: false) { } /// /// Creates a new instance of the class with associated query provider. /// - public QueryParameterExpression(string name, Type type, ParameterExpressionMode parameterExpressionMode) - : this(name, type, parameterExpressionMode, isNonNullableReferenceType: false) + public QueryParameterExpression(string name, Type type, ParameterTranslationMode parameterTranslationMode) + : this(name, type, parameterTranslationMode, isNonNullableReferenceType: false) { } @@ -33,11 +33,11 @@ public QueryParameterExpression(string name, Type type, ParameterExpressionMode /// Creates a new instance of the class with associated query provider. /// [Experimental(EFDiagnostics.PrecompiledQueryExperimental)] - public QueryParameterExpression(string name, Type type, ParameterExpressionMode? parameterExpressionMode, bool isNonNullableReferenceType) + public QueryParameterExpression(string name, Type type, ParameterTranslationMode? parameterTranslationMode, bool isNonNullableReferenceType) { Name = name; Type = type; - ParameterExpressionMode = parameterExpressionMode; + ParameterTranslationMode = parameterTranslationMode; IsNonNullableReferenceType = isNonNullableReferenceType; } @@ -62,7 +62,7 @@ public QueryParameterExpression(string name, Type type, ParameterExpressionMode? /// /// How should the parameter be handled. /// - public virtual ParameterExpressionMode? ParameterExpressionMode { get; } + public virtual ParameterTranslationMode? ParameterTranslationMode { get; } /// public override ExpressionType NodeType @@ -86,10 +86,10 @@ public override bool Equals(object? obj) private bool Equals(QueryParameterExpression queryParameterExpression) => Name == queryParameterExpression.Name && Type == queryParameterExpression.Type - && ParameterExpressionMode == queryParameterExpression.ParameterExpressionMode + && ParameterTranslationMode == queryParameterExpression.ParameterTranslationMode && IsNonNullableReferenceType == queryParameterExpression.IsNonNullableReferenceType; /// public override int GetHashCode() - => HashCode.Combine(Name, Type, ParameterExpressionMode, IsNonNullableReferenceType); + => HashCode.Combine(Name, Type, ParameterTranslationMode, IsNonNullableReferenceType); } diff --git a/test/EFCore.Relational.Specification.Tests/Query/AdHocMiscellaneousQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/AdHocMiscellaneousQueryRelationalTestBase.cs index 7476708138f..a572b1d9090 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/AdHocMiscellaneousQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/AdHocMiscellaneousQueryRelationalTestBase.cs @@ -20,7 +20,7 @@ protected void ClearLog() protected void AssertSql(params string[] expected) => TestSqlLoggerFactory.AssertBaseline(expected); - protected abstract DbContextOptionsBuilder SetParameterizedCollectionMode(DbContextOptionsBuilder optionsBuilder, ParameterizedCollectionMode parameterizedCollectionMode); + protected abstract DbContextOptionsBuilder SetParameterizedCollectionMode(DbContextOptionsBuilder optionsBuilder, ParameterTranslationMode parameterizedCollectionMode); #region 2951 @@ -277,7 +277,7 @@ public virtual async Task Check_inlined_constants_redacting(bool async, bool ena var contextFactory = await InitializeAsync( onConfiguring: o => { - SetParameterizedCollectionMode(o, ParameterizedCollectionMode.Constants); + SetParameterizedCollectionMode(o, ParameterTranslationMode.Constant); o.EnableSensitiveDataLogging(enableSensitiveDataLogging); }); using var context = contextFactory.CreateContext(); @@ -324,7 +324,7 @@ public class TestEntity public async Task Entity_equality_with_Contains_and_Parameter(bool async) { var contextFactory = await InitializeAsync( - onConfiguring: o => SetParameterizedCollectionMode(o, ParameterizedCollectionMode.Parameter)); + onConfiguring: o => SetParameterizedCollectionMode(o, ParameterTranslationMode.Parameter)); using var context = contextFactory.CreateContext(); List details = [new Context36311.BlogDetails { Id = 1 }, new Context36311.BlogDetails { Id = 2 }]; diff --git a/test/EFCore.Relational.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryRelationalTestBase.cs index 0904f628dd5..c45a27354de 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryRelationalTestBase.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.Linq; + namespace Microsoft.EntityFrameworkCore.Query; #nullable disable @@ -12,7 +14,7 @@ public abstract class NonSharedPrimitiveCollectionsQueryRelationalTestBase(NonSh public override Task Array_of_byte() => AssertTranslationFailed(() => TestArray((byte)1, (byte)2)); - protected abstract DbContextOptionsBuilder SetParameterizedCollectionMode(DbContextOptionsBuilder optionsBuilder, ParameterizedCollectionMode parameterizedCollectionMode); + protected abstract DbContextOptionsBuilder SetParameterizedCollectionMode(DbContextOptionsBuilder optionsBuilder, ParameterTranslationMode parameterizedCollectionMode); [ConditionalFact] public virtual async Task Column_collection_inside_json_owned_entity() @@ -37,11 +39,11 @@ public virtual async Task Column_collection_inside_json_owned_entity() } protected static IEnumerable ParameterizedCollectionModeValues() - => Enum.GetValues().Select(x => [x]); + => Enum.GetValues().Select(x => [x]); [ConditionalTheory] [MemberData(nameof(ParameterizedCollectionModeValues))] - public virtual async Task Parameter_collection_Count_with_column_predicate_with_default_mode(ParameterizedCollectionMode mode) + public virtual async Task Parameter_collection_Count_with_column_predicate_with_default_mode(ParameterTranslationMode mode) { var contextFactory = await InitializeAsync( onConfiguring: b => SetParameterizedCollectionMode(b, mode), @@ -62,7 +64,7 @@ public virtual async Task Parameter_collection_Count_with_column_predicate_with_ [ConditionalTheory] [MemberData(nameof(ParameterizedCollectionModeValues))] - public virtual async Task Parameter_collection_Contains_with_default_mode(ParameterizedCollectionMode mode) + public virtual async Task Parameter_collection_Contains_with_default_mode(ParameterTranslationMode mode) { var contextFactory = await InitializeAsync( onConfiguring: b => SetParameterizedCollectionMode(b, mode), @@ -84,7 +86,7 @@ public virtual async Task Parameter_collection_Contains_with_default_mode(Parame [ConditionalTheory] [MemberData(nameof(ParameterizedCollectionModeValues))] - public virtual async Task Parameter_collection_Count_with_column_predicate_with_default_mode_EF_Constant(ParameterizedCollectionMode mode) + public virtual async Task Parameter_collection_Count_with_column_predicate_with_default_mode_EF_Constant(ParameterTranslationMode mode) { var contextFactory = await InitializeAsync( onConfiguring: b => SetParameterizedCollectionMode(b, mode), @@ -105,7 +107,7 @@ public virtual async Task Parameter_collection_Count_with_column_predicate_with_ [ConditionalTheory] [MemberData(nameof(ParameterizedCollectionModeValues))] - public virtual async Task Parameter_collection_Contains_with_default_mode_EF_Constant(ParameterizedCollectionMode mode) + public virtual async Task Parameter_collection_Contains_with_default_mode_EF_Constant(ParameterTranslationMode mode) { var contextFactory = await InitializeAsync( onConfiguring: b => SetParameterizedCollectionMode(b, mode), @@ -127,7 +129,7 @@ public virtual async Task Parameter_collection_Contains_with_default_mode_EF_Con [ConditionalTheory] [MemberData(nameof(ParameterizedCollectionModeValues))] - public virtual async Task Parameter_collection_Count_with_column_predicate_with_default_mode_EF_Parameter(ParameterizedCollectionMode mode) + public virtual async Task Parameter_collection_Count_with_column_predicate_with_default_mode_EF_Parameter(ParameterTranslationMode mode) { var contextFactory = await InitializeAsync( onConfiguring: b => SetParameterizedCollectionMode(b, mode), @@ -149,7 +151,7 @@ public virtual async Task Parameter_collection_Count_with_column_predicate_with_ [ConditionalTheory] [MemberData(nameof(ParameterizedCollectionModeValues))] - public virtual async Task Parameter_collection_Contains_with_default_mode_EF_Parameter(ParameterizedCollectionMode mode) + public virtual async Task Parameter_collection_Contains_with_default_mode_EF_Parameter(ParameterTranslationMode mode) { var contextFactory = await InitializeAsync( onConfiguring: b => SetParameterizedCollectionMode(b, mode), @@ -171,7 +173,7 @@ public virtual async Task Parameter_collection_Contains_with_default_mode_EF_Par [ConditionalTheory] [MemberData(nameof(ParameterizedCollectionModeValues))] - public virtual async Task Parameter_collection_Count_with_column_predicate_with_default_mode_EF_MultipleParameters(ParameterizedCollectionMode mode) + public virtual async Task Parameter_collection_Count_with_column_predicate_with_default_mode_EF_MultipleParameters(ParameterTranslationMode mode) { var contextFactory = await InitializeAsync( onConfiguring: b => SetParameterizedCollectionMode(b, mode), @@ -193,7 +195,7 @@ public virtual async Task Parameter_collection_Count_with_column_predicate_with_ [ConditionalTheory] [MemberData(nameof(ParameterizedCollectionModeValues))] - public virtual async Task Parameter_collection_Contains_with_default_mode_EF_MultipleParameters(ParameterizedCollectionMode mode) + public virtual async Task Parameter_collection_Contains_with_default_mode_EF_MultipleParameters(ParameterTranslationMode mode) { var contextFactory = await InitializeAsync( onConfiguring: b => SetParameterizedCollectionMode(b, mode), diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocMiscellaneousQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/AdHocMiscellaneousQuerySqlServerTest.cs index cc0a312389c..d017fa4209d 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocMiscellaneousQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/AdHocMiscellaneousQuerySqlServerTest.cs @@ -19,7 +19,7 @@ public class AdHocMiscellaneousQuerySqlServerTest(NonSharedFixture fixture) : Ad protected override ITestStoreFactory TestStoreFactory => SqlServerTestStoreFactory.Instance; - protected override DbContextOptionsBuilder SetParameterizedCollectionMode(DbContextOptionsBuilder optionsBuilder, ParameterizedCollectionMode parameterizedCollectionMode) + protected override DbContextOptionsBuilder SetParameterizedCollectionMode(DbContextOptionsBuilder optionsBuilder, ParameterTranslationMode parameterizedCollectionMode) { new SqlServerDbContextOptionsBuilder(optionsBuilder).UseParameterizedCollectionMode(parameterizedCollectionMode); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs index 823ef7284d7..b7b86e420bc 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqlServerTest.cs @@ -10,7 +10,7 @@ namespace Microsoft.EntityFrameworkCore.Query; public class NonSharedPrimitiveCollectionsQuerySqlServerTest(NonSharedFixture fixture) : NonSharedPrimitiveCollectionsQueryRelationalTestBase(fixture) { - protected override DbContextOptionsBuilder SetParameterizedCollectionMode(DbContextOptionsBuilder optionsBuilder, ParameterizedCollectionMode parameterizedCollectionMode) + protected override DbContextOptionsBuilder SetParameterizedCollectionMode(DbContextOptionsBuilder optionsBuilder, ParameterTranslationMode parameterizedCollectionMode) { new SqlServerDbContextOptionsBuilder(optionsBuilder).UseParameterizedCollectionMode(parameterizedCollectionMode); @@ -785,13 +785,13 @@ FROM [TestEntityWithOwned] AS [t] """); } - public override async Task Parameter_collection_Count_with_column_predicate_with_default_mode(ParameterizedCollectionMode mode) + public override async Task Parameter_collection_Count_with_column_predicate_with_default_mode(ParameterTranslationMode mode) { await base.Parameter_collection_Count_with_column_predicate_with_default_mode(mode); switch (mode) { - case ParameterizedCollectionMode.Constants: + case ParameterTranslationMode.Constant: { AssertSql( """ @@ -805,7 +805,7 @@ SELECT COUNT(*) break; } - case ParameterizedCollectionMode.Parameter: + case ParameterTranslationMode.Parameter: { AssertSql( """ @@ -821,7 +821,7 @@ FROM OPENJSON(@ids) WITH ([value] int '$') AS [i] break; } - case ParameterizedCollectionMode.MultipleParameters: + case ParameterTranslationMode.MultipleParameters: { AssertSql( """ @@ -843,13 +843,13 @@ SELECT COUNT(*) } } - public override async Task Parameter_collection_Contains_with_default_mode(ParameterizedCollectionMode mode) + public override async Task Parameter_collection_Contains_with_default_mode(ParameterTranslationMode mode) { await base.Parameter_collection_Contains_with_default_mode(mode); switch (mode) { - case ParameterizedCollectionMode.Constants: + case ParameterTranslationMode.Constant: { AssertSql( """ @@ -860,7 +860,7 @@ WHERE [t].[Id] IN (2, 999) break; } - case ParameterizedCollectionMode.Parameter: + case ParameterTranslationMode.Parameter: { AssertSql( """ @@ -876,7 +876,7 @@ FROM OPENJSON(@ints) WITH ([value] int '$') AS [i] break; } - case ParameterizedCollectionMode.MultipleParameters: + case ParameterTranslationMode.MultipleParameters: { AssertSql( """ @@ -895,7 +895,7 @@ WHERE [t].[Id] IN (@ints1, @ints2) } } - public override async Task Parameter_collection_Count_with_column_predicate_with_default_mode_EF_Constant(ParameterizedCollectionMode mode) + public override async Task Parameter_collection_Count_with_column_predicate_with_default_mode_EF_Constant(ParameterTranslationMode mode) { await base.Parameter_collection_Count_with_column_predicate_with_default_mode_EF_Constant(mode); @@ -910,7 +910,7 @@ SELECT COUNT(*) """); } - public override async Task Parameter_collection_Contains_with_default_mode_EF_Constant(ParameterizedCollectionMode mode) + public override async Task Parameter_collection_Contains_with_default_mode_EF_Constant(ParameterTranslationMode mode) { await base.Parameter_collection_Contains_with_default_mode_EF_Constant(mode); @@ -922,7 +922,7 @@ WHERE [t].[Id] IN (2, 999) """); } - public override async Task Parameter_collection_Count_with_column_predicate_with_default_mode_EF_Parameter(ParameterizedCollectionMode mode) + public override async Task Parameter_collection_Count_with_column_predicate_with_default_mode_EF_Parameter(ParameterTranslationMode mode) { await base.Parameter_collection_Count_with_column_predicate_with_default_mode_EF_Parameter(mode); @@ -939,7 +939,7 @@ FROM OPENJSON(@ids) WITH ([value] int '$') AS [i] """); } - public override async Task Parameter_collection_Contains_with_default_mode_EF_Parameter(ParameterizedCollectionMode mode) + public override async Task Parameter_collection_Contains_with_default_mode_EF_Parameter(ParameterTranslationMode mode) { await base.Parameter_collection_Contains_with_default_mode_EF_Parameter(mode); @@ -956,7 +956,7 @@ FROM OPENJSON(@ints) WITH ([value] int '$') AS [i] """); } - public override async Task Parameter_collection_Count_with_column_predicate_with_default_mode_EF_MultipleParameters(ParameterizedCollectionMode mode) + public override async Task Parameter_collection_Count_with_column_predicate_with_default_mode_EF_MultipleParameters(ParameterTranslationMode mode) { await base.Parameter_collection_Count_with_column_predicate_with_default_mode_EF_MultipleParameters(mode); @@ -974,7 +974,7 @@ SELECT COUNT(*) """); } - public override async Task Parameter_collection_Contains_with_default_mode_EF_MultipleParameters(ParameterizedCollectionMode mode) + public override async Task Parameter_collection_Contains_with_default_mode_EF_MultipleParameters(ParameterTranslationMode mode) { await base.Parameter_collection_Contains_with_default_mode_EF_MultipleParameters(mode); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/AdHocMiscellaneousQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/AdHocMiscellaneousQuerySqliteTest.cs index e878daf1ff8..f80084d99e5 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/AdHocMiscellaneousQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/AdHocMiscellaneousQuerySqliteTest.cs @@ -10,7 +10,7 @@ public class AdHocMiscellaneousQuerySqliteTest(NonSharedFixture fixture) : AdHoc protected override ITestStoreFactory TestStoreFactory => SqliteTestStoreFactory.Instance; - protected override DbContextOptionsBuilder SetParameterizedCollectionMode(DbContextOptionsBuilder optionsBuilder, ParameterizedCollectionMode parameterizedCollectionMode) + protected override DbContextOptionsBuilder SetParameterizedCollectionMode(DbContextOptionsBuilder optionsBuilder, ParameterTranslationMode parameterizedCollectionMode) { new SqliteDbContextOptionsBuilder(optionsBuilder).UseParameterizedCollectionMode(parameterizedCollectionMode); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqliteTest.cs index 9cb695876e1..2f0eca42149 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NonSharedPrimitiveCollectionsQuerySqliteTest.cs @@ -7,7 +7,7 @@ namespace Microsoft.EntityFrameworkCore.Query; public class NonSharedPrimitiveCollectionsQuerySqliteTest(NonSharedFixture fixture) : NonSharedPrimitiveCollectionsQueryRelationalTestBase(fixture) { - protected override DbContextOptionsBuilder SetParameterizedCollectionMode(DbContextOptionsBuilder optionsBuilder, ParameterizedCollectionMode parameterizedCollectionMode) + protected override DbContextOptionsBuilder SetParameterizedCollectionMode(DbContextOptionsBuilder optionsBuilder, ParameterTranslationMode parameterizedCollectionMode) { new SqliteDbContextOptionsBuilder(optionsBuilder).UseParameterizedCollectionMode(parameterizedCollectionMode); @@ -329,13 +329,13 @@ LIMIT 2 """); } - public override async Task Parameter_collection_Count_with_column_predicate_with_default_mode(ParameterizedCollectionMode mode) + public override async Task Parameter_collection_Count_with_column_predicate_with_default_mode(ParameterTranslationMode mode) { await base.Parameter_collection_Count_with_column_predicate_with_default_mode(mode); switch (mode) { - case ParameterizedCollectionMode.Constants: + case ParameterTranslationMode.Constant: { AssertSql( """ @@ -349,7 +349,7 @@ SELECT COUNT(*) break; } - case ParameterizedCollectionMode.Parameter: + case ParameterTranslationMode.Parameter: { AssertSql( """ @@ -365,7 +365,7 @@ FROM json_each(@ids) AS "i" break; } - case ParameterizedCollectionMode.MultipleParameters: + case ParameterTranslationMode.MultipleParameters: { AssertSql( """ @@ -387,13 +387,13 @@ SELECT COUNT(*) } } - public override async Task Parameter_collection_Contains_with_default_mode(ParameterizedCollectionMode mode) + public override async Task Parameter_collection_Contains_with_default_mode(ParameterTranslationMode mode) { await base.Parameter_collection_Contains_with_default_mode(mode); switch (mode) { - case ParameterizedCollectionMode.Constants: + case ParameterTranslationMode.Constant: { AssertSql( """ @@ -404,7 +404,7 @@ public override async Task Parameter_collection_Contains_with_default_mode(Param break; } - case ParameterizedCollectionMode.Parameter: + case ParameterTranslationMode.Parameter: { AssertSql( """ @@ -420,7 +420,7 @@ FROM json_each(@ints) AS "i" break; } - case ParameterizedCollectionMode.MultipleParameters: + case ParameterTranslationMode.MultipleParameters: { AssertSql( """ @@ -439,7 +439,7 @@ FROM json_each(@ints) AS "i" } } - public override async Task Parameter_collection_Count_with_column_predicate_with_default_mode_EF_Constant(ParameterizedCollectionMode mode) + public override async Task Parameter_collection_Count_with_column_predicate_with_default_mode_EF_Constant(ParameterTranslationMode mode) { await base.Parameter_collection_Count_with_column_predicate_with_default_mode_EF_Constant(mode); @@ -454,7 +454,7 @@ SELECT COUNT(*) """); } - public override async Task Parameter_collection_Contains_with_default_mode_EF_Constant(ParameterizedCollectionMode mode) + public override async Task Parameter_collection_Contains_with_default_mode_EF_Constant(ParameterTranslationMode mode) { await base.Parameter_collection_Contains_with_default_mode_EF_Constant(mode); @@ -466,7 +466,7 @@ public override async Task Parameter_collection_Contains_with_default_mode_EF_Co """); } - public override async Task Parameter_collection_Count_with_column_predicate_with_default_mode_EF_Parameter(ParameterizedCollectionMode mode) + public override async Task Parameter_collection_Count_with_column_predicate_with_default_mode_EF_Parameter(ParameterTranslationMode mode) { await base.Parameter_collection_Count_with_column_predicate_with_default_mode_EF_Parameter(mode); @@ -483,7 +483,7 @@ FROM json_each(@ids) AS "i" """); } - public override async Task Parameter_collection_Contains_with_default_mode_EF_Parameter(ParameterizedCollectionMode mode) + public override async Task Parameter_collection_Contains_with_default_mode_EF_Parameter(ParameterTranslationMode mode) { await base.Parameter_collection_Contains_with_default_mode_EF_Parameter(mode); @@ -500,7 +500,7 @@ FROM json_each(@ints) AS "i" """); } - public override async Task Parameter_collection_Count_with_column_predicate_with_default_mode_EF_MultipleParameters(ParameterizedCollectionMode mode) + public override async Task Parameter_collection_Count_with_column_predicate_with_default_mode_EF_MultipleParameters(ParameterTranslationMode mode) { await base.Parameter_collection_Count_with_column_predicate_with_default_mode_EF_MultipleParameters(mode); @@ -518,7 +518,7 @@ SELECT COUNT(*) """); } - public override async Task Parameter_collection_Contains_with_default_mode_EF_MultipleParameters(ParameterizedCollectionMode mode) + public override async Task Parameter_collection_Contains_with_default_mode_EF_MultipleParameters(ParameterTranslationMode mode) { await base.Parameter_collection_Contains_with_default_mode_EF_MultipleParameters(mode); From 172c19eec1601ee316415378ebb95cb5538373a4 Mon Sep 17 00:00:00 2001 From: Jiri Cincura Date: Tue, 15 Jul 2025 14:14:34 +0200 Subject: [PATCH 09/10] Update type parameter on EF.MultipleParameters. --- src/EFCore.Relational/EFExtensions.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/EFCore.Relational/EFExtensions.cs b/src/EFCore.Relational/EFExtensions.cs index c2677cb8706..c790c6256f2 100644 --- a/src/EFCore.Relational/EFExtensions.cs +++ b/src/EFCore.Relational/EFExtensions.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.Collections; + namespace Microsoft.EntityFrameworkCore; /// @@ -25,10 +27,10 @@ public static class EFExtensions /// Within the context of an EF LINQ query, forces its argument to be inserted into the query as a multiple parameter expressions. /// /// Note that this is a static method accessed through the top-level static type. - /// The type of collection element. + /// The type of collection. /// The collection to be integrated as parameters into the query. /// The same value for further use in the query. - public static IEnumerable MultipleParameters(IEnumerable argument) + public static TSource MultipleParameters(TSource argument) where TSource : IEnumerable => throw new InvalidOperationException(RelationalStrings.EFMultipleParametersInvoked); } } From 1f9931362f00d52c4177eea314ff409b233f4792 Mon Sep 17 00:00:00 2001 From: Jiri Cincura Date: Wed, 16 Jul 2025 09:30:37 +0200 Subject: [PATCH 10/10] Feedback --- .../RelationalDbContextOptionsBuilder.cs | 2 +- .../Query/Internal/RelationalCommandCache.cs | 4 ++-- .../Internal/RelationalParameterProcessor.cs | 2 +- ...onalParameterBasedSqlProcessorParameters.cs | 8 ++++---- ...ryableMethodTranslatingExpressionVisitor.cs | 11 +++++------ ...nalShapedQueryCompilingExpressionVisitor.cs | 10 +++++----- ...elationalSqlTranslatingExpressionVisitor.cs | 4 ++-- .../SqlExpressions/SqlParameterExpression.cs | 12 ++++++------ .../Query/SqlNullabilityProcessor.cs | 12 +++++------- .../SqlServerSqlNullabilityProcessor.cs | 4 ++-- .../Internal/ExpressionTreeFuncletizer.cs | 2 +- ...ryableMethodNormalizingExpressionVisitor.cs | 2 +- src/EFCore/Query/QueryParameterExpression.cs | 16 ++++++++-------- ...mitiveCollectionsQueryRelationalTestBase.cs | 18 +++++++++--------- 14 files changed, 52 insertions(+), 55 deletions(-) diff --git a/src/EFCore.Relational/Infrastructure/RelationalDbContextOptionsBuilder.cs b/src/EFCore.Relational/Infrastructure/RelationalDbContextOptionsBuilder.cs index 3750bfd03aa..01935a24036 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalDbContextOptionsBuilder.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalDbContextOptionsBuilder.cs @@ -205,7 +205,7 @@ public virtual TBuilder TranslateParameterizedCollectionsToParameters() => UseParameterizedCollectionMode(ParameterTranslationMode.Parameter); /// - /// Configures mode to use when translating parameterized collections. + /// Configures the mode to use when translating parameterized collections. /// /// The same builder instance so that multiple calls can be chained. public virtual TBuilder UseParameterizedCollectionMode(ParameterTranslationMode parameterizedCollectionMode) diff --git a/src/EFCore.Relational/Query/Internal/RelationalCommandCache.cs b/src/EFCore.Relational/Query/Internal/RelationalCommandCache.cs index 283920a033a..e7ba5c61764 100644 --- a/src/EFCore.Relational/Query/Internal/RelationalCommandCache.cs +++ b/src/EFCore.Relational/Query/Internal/RelationalCommandCache.cs @@ -35,13 +35,13 @@ public RelationalCommandCache( IRelationalParameterBasedSqlProcessorFactory relationalParameterBasedSqlProcessorFactory, Expression queryExpression, bool useRelationalNulls, - ParameterTranslationMode parameterizedCollectionMode) + ParameterTranslationMode collectionParameterTranslationMode) { _memoryCache = memoryCache; _querySqlGeneratorFactory = querySqlGeneratorFactory; _queryExpression = queryExpression; _relationalParameterBasedSqlProcessor = relationalParameterBasedSqlProcessorFactory.Create( - new RelationalParameterBasedSqlProcessorParameters(useRelationalNulls, parameterizedCollectionMode)); + new RelationalParameterBasedSqlProcessorParameters(useRelationalNulls, collectionParameterTranslationMode)); } /// diff --git a/src/EFCore.Relational/Query/Internal/RelationalParameterProcessor.cs b/src/EFCore.Relational/Query/Internal/RelationalParameterProcessor.cs index f537a04f3be..f1ed4b087b1 100644 --- a/src/EFCore.Relational/Query/Internal/RelationalParameterProcessor.cs +++ b/src/EFCore.Relational/Query/Internal/RelationalParameterProcessor.cs @@ -128,7 +128,7 @@ private SqlParameterExpression VisitSqlParameter(SqlParameterExpression paramete uniquifiedName, parameter.Type, parameter.IsNullable, - parameter.ParameterTranslationMode, + parameter.TranslationMode, parameter.TypeMapping); return _sqlParameters[newParameter.InvariantName] = newParameter; diff --git a/src/EFCore.Relational/Query/RelationalParameterBasedSqlProcessorParameters.cs b/src/EFCore.Relational/Query/RelationalParameterBasedSqlProcessorParameters.cs index eb5f9ae57bf..d0bc742fd83 100644 --- a/src/EFCore.Relational/Query/RelationalParameterBasedSqlProcessorParameters.cs +++ b/src/EFCore.Relational/Query/RelationalParameterBasedSqlProcessorParameters.cs @@ -16,17 +16,17 @@ public sealed record RelationalParameterBasedSqlProcessorParameters /// /// Which parametrized collection translation mode should be used. /// - public ParameterTranslationMode ParameterizedCollectionMode { get; init; } + public ParameterTranslationMode CollectionParameterTranslationMode { get; init; } /// /// Creates a new instance of . /// /// A value indicating if relational nulls should be used. - /// Which translation mode should be used. + /// Which translation mode should be used. [EntityFrameworkInternal] - public RelationalParameterBasedSqlProcessorParameters(bool useRelationalNulls, ParameterTranslationMode parameterizedCollectionMode) + public RelationalParameterBasedSqlProcessorParameters(bool useRelationalNulls, ParameterTranslationMode collectionParameterTranslationMode) { UseRelationalNulls = useRelationalNulls; - ParameterizedCollectionMode = parameterizedCollectionMode; + CollectionParameterTranslationMode = collectionParameterTranslationMode; } } diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index 73eb34ccf7a..18bb222f2f1 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -21,7 +21,7 @@ public partial class RelationalQueryableMethodTranslatingExpressionVisitor : Que private readonly IRelationalTypeMappingSource _typeMappingSource; private readonly ISqlExpressionFactory _sqlExpressionFactory; private readonly bool _subquery; - private readonly ParameterTranslationMode _parameterizedCollectionMode; + private readonly ParameterTranslationMode _collectionParameterTranslationMode; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -64,7 +64,7 @@ public RelationalQueryableMethodTranslatingExpressionVisitor( _typeMappingSource = relationalDependencies.TypeMappingSource; _sqlExpressionFactory = sqlExpressionFactory; _subquery = false; - _parameterizedCollectionMode = RelationalOptionsExtension.Extract(queryCompilationContext.ContextOptions).ParameterizedCollectionMode; + _collectionParameterTranslationMode = RelationalOptionsExtension.Extract(queryCompilationContext.ContextOptions).ParameterizedCollectionMode; } /// @@ -90,7 +90,7 @@ protected RelationalQueryableMethodTranslatingExpressionVisitor( _typeMappingSource = parentVisitor._typeMappingSource; _sqlExpressionFactory = parentVisitor._sqlExpressionFactory; _subquery = true; - _parameterizedCollectionMode = RelationalOptionsExtension.Extract(parentVisitor._queryCompilationContext.ContextOptions).ParameterizedCollectionMode; + _collectionParameterTranslationMode = RelationalOptionsExtension.Extract(parentVisitor._queryCompilationContext.ContextOptions).ParameterizedCollectionMode; } /// @@ -244,7 +244,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp && methodCallExpression.Arguments[0] is ParameterQueryRootExpression parameterSource && TranslateExpression(methodCallExpression.Arguments[1]) is SqlExpression item && _sqlTranslator.Visit(parameterSource.QueryParameterExpression) is SqlParameterExpression sqlParameterExpression - && (parameterSource.QueryParameterExpression.ParameterTranslationMode is ParameterTranslationMode.Constant + && (parameterSource.QueryParameterExpression.TranslationMode is ParameterTranslationMode.Constant or null)) { var inExpression = _sqlExpressionFactory.In(item, sqlParameterExpression); @@ -299,8 +299,7 @@ JsonScalarExpression jsonScalar var tableAlias = _sqlAliasManager.GenerateTableAlias(sqlParameterExpression.Name.TrimStart('_')); - var parameterMode = queryParameter.ParameterTranslationMode ?? _parameterizedCollectionMode; - return parameterMode switch + return (queryParameter.TranslationMode ?? _collectionParameterTranslationMode) switch { ParameterTranslationMode.Constant or ParameterTranslationMode.MultipleParameters => CreateShapedQueryExpressionForValuesExpression( diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs index 1eadeaf41ad..dbbe1c92358 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs @@ -19,7 +19,7 @@ public partial class RelationalShapedQueryCompilingExpressionVisitor : ShapedQue private readonly bool _threadSafetyChecksEnabled; private readonly bool _detailedErrorsEnabled; private readonly bool _useRelationalNulls; - private readonly ParameterTranslationMode _parameterizedCollectionMode; + private readonly ParameterTranslationMode _collectionParameterTranslationMode; private readonly bool _isPrecompiling; private readonly RelationalParameterBasedSqlProcessor _relationalParameterBasedSqlProcessor; @@ -55,7 +55,7 @@ public RelationalShapedQueryCompilingExpressionVisitor( _relationalParameterBasedSqlProcessor = relationalDependencies.RelationalParameterBasedSqlProcessorFactory.Create( - new RelationalParameterBasedSqlProcessorParameters(_useRelationalNulls, _parameterizedCollectionMode)); + new RelationalParameterBasedSqlProcessorParameters(_useRelationalNulls, _collectionParameterTranslationMode)); _querySqlGeneratorFactory = relationalDependencies.QuerySqlGeneratorFactory; _contextType = queryCompilationContext.ContextType; @@ -63,7 +63,7 @@ public RelationalShapedQueryCompilingExpressionVisitor( _threadSafetyChecksEnabled = dependencies.CoreSingletonOptions.AreThreadSafetyChecksEnabled; _detailedErrorsEnabled = dependencies.CoreSingletonOptions.AreDetailedErrorsEnabled; _useRelationalNulls = RelationalOptionsExtension.Extract(queryCompilationContext.ContextOptions).UseRelationalNulls; - _parameterizedCollectionMode = RelationalOptionsExtension.Extract(queryCompilationContext.ContextOptions).ParameterizedCollectionMode; + _collectionParameterTranslationMode = RelationalOptionsExtension.Extract(queryCompilationContext.ContextOptions).ParameterizedCollectionMode; _isPrecompiling = queryCompilationContext.IsPrecompiling; } @@ -500,7 +500,7 @@ private Expression CreateRelationalCommandResolverExpression(Expression queryExp RelationalDependencies.RelationalParameterBasedSqlProcessorFactory, queryExpression, _useRelationalNulls, - _parameterizedCollectionMode); + _collectionParameterTranslationMode); var commandLiftableConstant = RelationalDependencies.RelationalLiftableConstantFactory.CreateLiftableConstant( relationalCommandCache, @@ -751,7 +751,7 @@ Expression> Generate _relationalDependenciesRelationalParameterBasedSqlProcessorFactoryProperty), Constant(queryExpression), Constant(_useRelationalNulls), - Constant(_parameterizedCollectionMode, typeof(ParameterTranslationMode))), + Constant(_collectionParameterTranslationMode, typeof(ParameterTranslationMode))), contextParameter); } } diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs index 5072dc0b55e..fb89e8c214e 100644 --- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs @@ -516,7 +516,7 @@ protected override Expression VisitExtension(Expression extensionExpression) name: queryParameter.Name, queryParameter.Type, nullable: false, - queryParameter.ParameterTranslationMode, + queryParameter.TranslationMode, typeMapping: null); } @@ -525,7 +525,7 @@ protected override Expression VisitExtension(Expression extensionExpression) name: queryParameter.Name, queryParameter.Type, queryParameter.Type.IsNullableType(), - queryParameter.ParameterTranslationMode, + queryParameter.TranslationMode, typeMapping: null); case StructuralTypeShaperExpression shaper: diff --git a/src/EFCore.Relational/Query/SqlExpressions/SqlParameterExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SqlParameterExpression.cs index 0c32405bca7..a279f21889f 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SqlParameterExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SqlParameterExpression.cs @@ -17,7 +17,7 @@ public sealed class SqlParameterExpression : SqlExpression /// The of the expression. /// The associated with the expression. public SqlParameterExpression(string name, Type type, RelationalTypeMapping? typeMapping) - : this(invariantName: name, name: name, type.UnwrapNullableType(), type.IsNullableType(), parameterTranslationMode: null, typeMapping) + : this(invariantName: name, name: name, type.UnwrapNullableType(), type.IsNullableType(), translationMode: null, typeMapping) { } @@ -31,21 +31,21 @@ public SqlParameterExpression(string name, Type type, RelationalTypeMapping? typ /// /// The of the expression. /// Whether this parameter can have null values. - /// How the parameter should be handled. + /// How the parameter should be handled. /// The associated with the expression. public SqlParameterExpression( string invariantName, string name, Type type, bool nullable, - ParameterTranslationMode? parameterTranslationMode, + ParameterTranslationMode? translationMode, RelationalTypeMapping? typeMapping) : base(type.UnwrapNullableType(), typeMapping) { InvariantName = invariantName; Name = name; IsNullable = nullable; - ParameterTranslationMode = parameterTranslationMode; + TranslationMode = translationMode; } /// @@ -67,7 +67,7 @@ public SqlParameterExpression( /// /// How the parameter should be handled. /// - public ParameterTranslationMode? ParameterTranslationMode { get; } + public ParameterTranslationMode? TranslationMode { get; } /// /// Applies supplied type mapping to this expression. @@ -75,7 +75,7 @@ public SqlParameterExpression( /// A relational type mapping to apply. /// A new expression which has supplied type mapping. public SqlExpression ApplyTypeMapping(RelationalTypeMapping? typeMapping) - => new SqlParameterExpression(InvariantName, Name, Type, IsNullable, ParameterTranslationMode, typeMapping); + => new SqlParameterExpression(InvariantName, Name, Type, IsNullable, TranslationMode, typeMapping); /// protected override Expression VisitChildren(ExpressionVisitor visitor) diff --git a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs index 1c6e62e0b79..190d56525b8 100644 --- a/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs +++ b/src/EFCore.Relational/Query/SqlNullabilityProcessor.cs @@ -39,7 +39,7 @@ public SqlNullabilityProcessor( { Dependencies = dependencies; UseRelationalNulls = parameters.UseRelationalNulls; - ParameterizedCollectionMode = parameters.ParameterizedCollectionMode; + CollectionParameterTranslationMode = parameters.CollectionParameterTranslationMode; _sqlExpressionFactory = dependencies.SqlExpressionFactory; _nonNullableColumns = []; @@ -61,7 +61,7 @@ public SqlNullabilityProcessor( /// /// A value indicating what translation mode to use. /// - public virtual ParameterTranslationMode ParameterizedCollectionMode { get; } + public virtual ParameterTranslationMode CollectionParameterTranslationMode { get; } /// /// Dictionary of current parameter values in use. @@ -126,8 +126,7 @@ protected override Expression VisitExtension(Expression node) var processedValues = new List(); - var parameterMode = valuesParameter.ParameterTranslationMode ?? ParameterizedCollectionMode; - switch (parameterMode) + switch (valuesParameter.TranslationMode ?? CollectionParameterTranslationMode) { case ParameterTranslationMode.MultipleParameters: { @@ -815,7 +814,6 @@ InExpression ProcessInExpressionValues( processedValues = []; - var parameterMode = valuesParameter.ParameterTranslationMode ?? ParameterizedCollectionMode; var expandedParameters = _collectionParameterExpansionMap.GetOrAddNew(valuesParameter); var expandedParametersCounter = 0; for (var i = 0; i < values.Count; i++) @@ -826,7 +824,7 @@ InExpression ProcessInExpressionValues( continue; } - switch (parameterMode) + switch (valuesParameter.TranslationMode ?? CollectionParameterTranslationMode) { case ParameterTranslationMode.MultipleParameters: // see #36311 for more info @@ -1410,7 +1408,7 @@ protected virtual SqlExpression VisitSqlParameter( nullable = false; - if (sqlParameterExpression.ParameterTranslationMode is ParameterTranslationMode.Constant) + if (sqlParameterExpression.TranslationMode is ParameterTranslationMode.Constant) { var parameters = ParametersFacade.GetParametersAndDisableSqlCaching(); diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs index 35375ed6b11..7c89b8987b9 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerSqlNullabilityProcessor.cs @@ -190,7 +190,7 @@ protected override Expression VisitExtension(Expression node) switch (node) { case ValuesExpression { ValuesParameter: SqlParameterExpression valuesParameter } valuesExpression - when (valuesParameter.ParameterTranslationMode ?? ParameterizedCollectionMode) is ParameterTranslationMode.MultipleParameters: + when (valuesParameter.TranslationMode ?? CollectionParameterTranslationMode) is ParameterTranslationMode.MultipleParameters: { Check.DebugAssert(valuesParameter.TypeMapping is not null); Check.DebugAssert(valuesParameter.TypeMapping.ElementTypeMapping is not null); @@ -236,7 +236,7 @@ protected override SqlExpression VisitIn(InExpression inExpression, bool allowOp switch (inExpression.ValuesParameter) { case SqlParameterExpression valuesParameter - when (valuesParameter.ParameterTranslationMode ?? ParameterizedCollectionMode) is ParameterTranslationMode.MultipleParameters: + when (valuesParameter.TranslationMode ?? CollectionParameterTranslationMode) is ParameterTranslationMode.MultipleParameters: { Check.DebugAssert(valuesParameter.TypeMapping is not null); Check.DebugAssert(valuesParameter.TypeMapping.ElementTypeMapping is not null); diff --git a/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs b/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs index d4250c25686..afb319d52c0 100644 --- a/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs +++ b/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs @@ -1988,7 +1988,7 @@ private static StateType CombineStateTypes(StateType stateType1, StateType state return _parameterizedValues[evaluatableRoot] = new QueryParameterExpression( parameterName, evaluatableRoot.Type, - parameterTranslationMode: null, + translationMode: null, isNonNullableReferenceType); } diff --git a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs index d2fd64b13f6..067b9dd3cad 100644 --- a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs @@ -128,7 +128,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp var queryParameter = (QueryParameterExpression)Visit(methodCallExpression.Arguments[0]); return new QueryParameterExpression( - queryParameter.Name, queryParameter.Type, parameterTranslationMode: ParameterTranslationMode.Constant, + queryParameter.Name, queryParameter.Type, translationMode: ParameterTranslationMode.Constant, queryParameter.IsNonNullableReferenceType); } diff --git a/src/EFCore/Query/QueryParameterExpression.cs b/src/EFCore/Query/QueryParameterExpression.cs index a15248cf241..dde4c03c09f 100644 --- a/src/EFCore/Query/QueryParameterExpression.cs +++ b/src/EFCore/Query/QueryParameterExpression.cs @@ -17,15 +17,15 @@ public class QueryParameterExpression : Expression, IPrintableExpression /// Creates a new instance of the class with associated query provider. /// public QueryParameterExpression(string name, Type type) - : this(name, type, parameterTranslationMode: null, isNonNullableReferenceType: false) + : this(name, type, translationMode: null, isNonNullableReferenceType: false) { } /// /// Creates a new instance of the class with associated query provider. /// - public QueryParameterExpression(string name, Type type, ParameterTranslationMode parameterTranslationMode) - : this(name, type, parameterTranslationMode, isNonNullableReferenceType: false) + public QueryParameterExpression(string name, Type type, ParameterTranslationMode translationMode) + : this(name, type, translationMode, isNonNullableReferenceType: false) { } @@ -33,11 +33,11 @@ public QueryParameterExpression(string name, Type type, ParameterTranslationMode /// Creates a new instance of the class with associated query provider. /// [Experimental(EFDiagnostics.PrecompiledQueryExperimental)] - public QueryParameterExpression(string name, Type type, ParameterTranslationMode? parameterTranslationMode, bool isNonNullableReferenceType) + public QueryParameterExpression(string name, Type type, ParameterTranslationMode? translationMode, bool isNonNullableReferenceType) { Name = name; Type = type; - ParameterTranslationMode = parameterTranslationMode; + TranslationMode = translationMode; IsNonNullableReferenceType = isNonNullableReferenceType; } @@ -62,7 +62,7 @@ public QueryParameterExpression(string name, Type type, ParameterTranslationMode /// /// How should the parameter be handled. /// - public virtual ParameterTranslationMode? ParameterTranslationMode { get; } + public virtual ParameterTranslationMode? TranslationMode { get; } /// public override ExpressionType NodeType @@ -86,10 +86,10 @@ public override bool Equals(object? obj) private bool Equals(QueryParameterExpression queryParameterExpression) => Name == queryParameterExpression.Name && Type == queryParameterExpression.Type - && ParameterTranslationMode == queryParameterExpression.ParameterTranslationMode + && TranslationMode == queryParameterExpression.TranslationMode && IsNonNullableReferenceType == queryParameterExpression.IsNonNullableReferenceType; /// public override int GetHashCode() - => HashCode.Combine(Name, Type, ParameterTranslationMode, IsNonNullableReferenceType); + => HashCode.Combine(Name, Type, TranslationMode, IsNonNullableReferenceType); } diff --git a/test/EFCore.Relational.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryRelationalTestBase.cs index c45a27354de..e5132067c77 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/NonSharedPrimitiveCollectionsQueryRelationalTestBase.cs @@ -38,11 +38,11 @@ public virtual async Task Column_collection_inside_json_owned_entity() Assert.Equivalent(new[] { "foo", "bar" }, result.Owned.Strings); } - protected static IEnumerable ParameterizedCollectionModeValues() + protected static IEnumerable ParameterTranslationModeValues() => Enum.GetValues().Select(x => [x]); [ConditionalTheory] - [MemberData(nameof(ParameterizedCollectionModeValues))] + [MemberData(nameof(ParameterTranslationModeValues))] public virtual async Task Parameter_collection_Count_with_column_predicate_with_default_mode(ParameterTranslationMode mode) { var contextFactory = await InitializeAsync( @@ -63,7 +63,7 @@ public virtual async Task Parameter_collection_Count_with_column_predicate_with_ } [ConditionalTheory] - [MemberData(nameof(ParameterizedCollectionModeValues))] + [MemberData(nameof(ParameterTranslationModeValues))] public virtual async Task Parameter_collection_Contains_with_default_mode(ParameterTranslationMode mode) { var contextFactory = await InitializeAsync( @@ -85,7 +85,7 @@ public virtual async Task Parameter_collection_Contains_with_default_mode(Parame } [ConditionalTheory] - [MemberData(nameof(ParameterizedCollectionModeValues))] + [MemberData(nameof(ParameterTranslationModeValues))] public virtual async Task Parameter_collection_Count_with_column_predicate_with_default_mode_EF_Constant(ParameterTranslationMode mode) { var contextFactory = await InitializeAsync( @@ -106,7 +106,7 @@ public virtual async Task Parameter_collection_Count_with_column_predicate_with_ } [ConditionalTheory] - [MemberData(nameof(ParameterizedCollectionModeValues))] + [MemberData(nameof(ParameterTranslationModeValues))] public virtual async Task Parameter_collection_Contains_with_default_mode_EF_Constant(ParameterTranslationMode mode) { var contextFactory = await InitializeAsync( @@ -128,7 +128,7 @@ public virtual async Task Parameter_collection_Contains_with_default_mode_EF_Con } [ConditionalTheory] - [MemberData(nameof(ParameterizedCollectionModeValues))] + [MemberData(nameof(ParameterTranslationModeValues))] public virtual async Task Parameter_collection_Count_with_column_predicate_with_default_mode_EF_Parameter(ParameterTranslationMode mode) { var contextFactory = await InitializeAsync( @@ -150,7 +150,7 @@ public virtual async Task Parameter_collection_Count_with_column_predicate_with_ } [ConditionalTheory] - [MemberData(nameof(ParameterizedCollectionModeValues))] + [MemberData(nameof(ParameterTranslationModeValues))] public virtual async Task Parameter_collection_Contains_with_default_mode_EF_Parameter(ParameterTranslationMode mode) { var contextFactory = await InitializeAsync( @@ -172,7 +172,7 @@ public virtual async Task Parameter_collection_Contains_with_default_mode_EF_Par } [ConditionalTheory] - [MemberData(nameof(ParameterizedCollectionModeValues))] + [MemberData(nameof(ParameterTranslationModeValues))] public virtual async Task Parameter_collection_Count_with_column_predicate_with_default_mode_EF_MultipleParameters(ParameterTranslationMode mode) { var contextFactory = await InitializeAsync( @@ -194,7 +194,7 @@ public virtual async Task Parameter_collection_Count_with_column_predicate_with_ } [ConditionalTheory] - [MemberData(nameof(ParameterizedCollectionModeValues))] + [MemberData(nameof(ParameterTranslationModeValues))] public virtual async Task Parameter_collection_Contains_with_default_mode_EF_MultipleParameters(ParameterTranslationMode mode) { var contextFactory = await InitializeAsync(