diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index 7ee0350c894..7b60363860e 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -22,6 +22,9 @@ public class RelationalQueryableMethodTranslatingExpressionVisitor : QueryableMe private readonly ISqlExpressionFactory _sqlExpressionFactory; private readonly bool _subquery; + private static readonly bool UseOldBehavior32218 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue32218", out var enabled32218) && enabled32218; + /// /// Creates a new instance of the class. /// @@ -288,7 +291,9 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp // Server), we need to fall back to the previous IN translation. if (method.IsGenericMethod && method.GetGenericMethodDefinition() == QueryableMethods.Contains - && methodCallExpression.Arguments[0] is ParameterQueryRootExpression parameterSource + && (UseOldBehavior32218 + ? methodCallExpression.Arguments[0] + : UnwrapAsQueryable(methodCallExpression.Arguments[0])) is ParameterQueryRootExpression parameterSource && TranslateExpression(methodCallExpression.Arguments[1]) is SqlExpression item && _sqlTranslator.Visit(parameterSource.ParameterExpression) is SqlParameterExpression sqlParameterExpression) { @@ -300,6 +305,12 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp .UpdateResultCardinality(ResultCardinality.Single); return shapedQueryExpression; } + + static Expression UnwrapAsQueryable(Expression expression) + => expression is MethodCallExpression { Method: { IsGenericMethod: true } method } methodCall + && method.GetGenericMethodDefinition() == QueryableMethods.AsQueryable + ? methodCall.Arguments[0] + : expression; } return translated; diff --git a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs index f89f9733d2d..28c8d830258 100644 --- a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs @@ -19,6 +19,9 @@ public class QueryableMethodNormalizingExpressionVisitor : ExpressionVisitor private readonly SelectManyVerifyingExpressionVisitor _selectManyVerifyingExpressionVisitor = new(); private readonly GroupJoinConvertingExpressionVisitor _groupJoinConvertingExpressionVisitor = new(); + private static readonly bool UseOldBehavior32215 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue32215", out var enabled32215) && enabled32215; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -435,12 +438,14 @@ private Expression TryConvertListContainsToQueryableContains(MethodCallExpressio var sourceType = methodCallExpression.Method.DeclaringType!.GetGenericArguments()[0]; - return Expression.Call( + var converted = Expression.Call( QueryableMethods.Contains.MakeGenericMethod(sourceType), Expression.Call( QueryableMethods.AsQueryable.MakeGenericMethod(sourceType), methodCallExpression.Object!), methodCallExpression.Arguments[0]); + + return UseOldBehavior32215 ? converted : VisitMethodCall(converted); } private static bool CanConvertEnumerableToQueryable(Type enumerableType, Type queryableType) diff --git a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs index 943bf5b8af0..0e302e2becf 100644 --- a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs @@ -807,6 +807,34 @@ public virtual Task Project_primitive_collections_element(bool async) }, assertOrder: true); + [ConditionalTheory] // #32208, #32215 + [MemberData(nameof(IsAsyncData))] + public virtual Task Nested_contains_with_Lists_and_no_inferred_type_mapping(bool async) + { + var ints = new List { 1, 2, 3 }; + var strings = new List { "one", "two", "three" }; + + // Note that in this query, the outer Contains really has no type mapping, neither for its source (collection parameter), nor + // for its item (the conditional expression returns constants). The default type mapping must be applied. + return AssertQuery( + async, + ss => ss.Set().Where(e => strings.Contains(ints.Contains(e.Int) ? "one" : "two"))); + } + + [ConditionalTheory] // #32208, #32215 + [MemberData(nameof(IsAsyncData))] + public virtual Task Nested_contains_with_arrays_and_no_inferred_type_mapping(bool async) + { + var ints = new[] { 1, 2, 3 }; + var strings = new[] { "one", "two", "three" }; + + // Note that in this query, the outer Contains really has no type mapping, neither for its source (collection parameter), nor + // for its item (the conditional expression returns constants). The default type mapping must be applied. + return AssertQuery( + async, + ss => ss.Set().Where(e => strings.Contains(ints.Contains(e.Int) ? "one" : "two"))); + } + public abstract class PrimitiveCollectionsQueryFixtureBase : SharedStoreFixtureBase, IQueryFixtureBase { private PrimitiveArrayData? _expectedData; diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs index 894782c9ee4..2117994b0e9 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs @@ -610,6 +610,36 @@ ORDER BY [p].[Id] """); } + public override async Task Nested_contains_with_Lists_and_no_inferred_type_mapping(bool async) + { + await base.Nested_contains_with_Lists_and_no_inferred_type_mapping(async); + + AssertSql( + """ +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE CASE + WHEN [p].[Int] IN (1, 2, 3) THEN N'one' + ELSE N'two' +END IN (N'one', N'two', N'three') +"""); + } + + public override async Task Nested_contains_with_arrays_and_no_inferred_type_mapping(bool async) + { + await base.Nested_contains_with_arrays_and_no_inferred_type_mapping(async); + + AssertSql( + """ +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE CASE + WHEN [p].[Int] IN (1, 2, 3) THEN N'one' + ELSE N'two' +END IN (N'one', N'two', N'three') +"""); + } + [ConditionalFact] public virtual void Check_all_tests_overridden() => TestHelpers.AssertAllMethodsOverridden(GetType()); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs index 4f045e011cb..970e6cbdbc9 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs @@ -1227,6 +1227,54 @@ ORDER BY [p].[Id] """); } + public override async Task Nested_contains_with_Lists_and_no_inferred_type_mapping(bool async) + { + await base.Nested_contains_with_Lists_and_no_inferred_type_mapping(async); + + AssertSql( + """ +@__ints_1='[1,2,3]' (Size = 4000) +@__strings_0='["one","two","three"]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE CASE + WHEN [p].[Int] IN ( + SELECT [i].[value] + FROM OPENJSON(@__ints_1) WITH ([value] int '$') AS [i] + ) THEN N'one' + ELSE N'two' +END IN ( + SELECT [s].[value] + FROM OPENJSON(@__strings_0) WITH ([value] nvarchar(max) '$') AS [s] +) +"""); + } + + public override async Task Nested_contains_with_arrays_and_no_inferred_type_mapping(bool async) + { + await base.Nested_contains_with_arrays_and_no_inferred_type_mapping(async); + + AssertSql( + """ +@__ints_1='[1,2,3]' (Size = 4000) +@__strings_0='["one","two","three"]' (Size = 4000) + +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE CASE + WHEN [p].[Int] IN ( + SELECT [i].[value] + FROM OPENJSON(@__ints_1) WITH ([value] int '$') AS [i] + ) THEN N'one' + ELSE N'two' +END IN ( + SELECT [s].[value] + FROM OPENJSON(@__strings_0) WITH ([value] nvarchar(max) '$') AS [s] +) +"""); + } + [ConditionalFact] public virtual void Check_all_tests_overridden() => TestHelpers.AssertAllMethodsOverridden(GetType()); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs index dc5f33e5b03..8ade2106275 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs @@ -3982,13 +3982,17 @@ public virtual async Task Nested_contains_with_enum() AssertSql( """ +@__todoTypes_1='[0]' (Size = 4000) @__key_2='5f221fb9-66f4-442a-92c9-d97ed5989cc7' @__keys_0='["0a47bcb7-a1cb-4345-8944-c58f82d6aac7","5f221fb9-66f4-442a-92c9-d97ed5989cc7"]' (Size = 4000) SELECT [t].[Id], [t].[Type] FROM [Todos] AS [t] WHERE CASE - WHEN [t].[Type] = 0 THEN @__key_2 + WHEN [t].[Type] IN ( + SELECT [t0].[value] + FROM OPENJSON(@__todoTypes_1) WITH ([value] int '$') AS [t0] + ) THEN @__key_2 ELSE @__key_2 END IN ( SELECT [k].[value] diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs index a6eb534b7b4..a6ac3d00ff9 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs @@ -1109,6 +1109,54 @@ public override async Task Project_empty_collection_of_nullables_and_collection_ (await Assert.ThrowsAsync( () => base.Project_empty_collection_of_nullables_and_collection_only_containing_nulls(async))).Message); + public override async Task Nested_contains_with_Lists_and_no_inferred_type_mapping(bool async) + { + await base.Nested_contains_with_Lists_and_no_inferred_type_mapping(async); + + AssertSql( + """ +@__ints_1='[1,2,3]' (Size = 7) +@__strings_0='["one","two","three"]' (Size = 21) + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."NullableString", "p"."NullableStrings", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE CASE + WHEN "p"."Int" IN ( + SELECT "i"."value" + FROM json_each(@__ints_1) AS "i" + ) THEN 'one' + ELSE 'two' +END IN ( + SELECT "s"."value" + FROM json_each(@__strings_0) AS "s" +) +"""); + } + + public override async Task Nested_contains_with_arrays_and_no_inferred_type_mapping(bool async) + { + await base.Nested_contains_with_arrays_and_no_inferred_type_mapping(async); + + AssertSql( + """ + @__ints_1='[1,2,3]' (Size = 7) + @__strings_0='["one","two","three"]' (Size = 21) + + SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."NullableString", "p"."NullableStrings", "p"."String", "p"."Strings" + FROM "PrimitiveCollectionsEntity" AS "p" + WHERE CASE + WHEN "p"."Int" IN ( + SELECT "i"."value" + FROM json_each(@__ints_1) AS "i" + ) THEN 'one' + ELSE 'two' + END IN ( + SELECT "s"."value" + FROM json_each(@__strings_0) AS "s" + ) + """); + } + [ConditionalFact] public virtual void Check_all_tests_overridden() => TestHelpers.AssertAllMethodsOverridden(GetType());