diff --git a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs index ad9d12fc3ae..7a43eab9d98 100644 --- a/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs @@ -20,6 +20,9 @@ public class QueryableMethodNormalizingExpressionVisitor : ExpressionVisitor private readonly SelectManyVerifyingExpressionVisitor _selectManyVerifyingExpressionVisitor = new(); private readonly GroupJoinConvertingExpressionVisitor _groupJoinConvertingExpressionVisitor = new(); + private static readonly bool UseOldBehavior35102 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue35102", out var enabled35102) && enabled35102; + /// /// 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 @@ -489,12 +492,16 @@ private Expression TryConvertCollectionContainsToQueryableContains(MethodCallExp var sourceType = methodCallExpression.Method.DeclaringType!.GetGenericArguments()[0]; + var objectExpression = methodCallExpression.Object!.Type.IsValueType && !UseOldBehavior35102 + ? Expression.Convert(methodCallExpression.Object!, typeof(IEnumerable<>).MakeGenericType(sourceType)) + : methodCallExpression.Object!; + return VisitMethodCall( Expression.Call( QueryableMethods.Contains.MakeGenericMethod(sourceType), Expression.Call( QueryableMethods.AsQueryable.MakeGenericMethod(sourceType), - methodCallExpression.Object!), + objectExpression), methodCallExpression.Arguments[0])); } diff --git a/src/EFCore/Query/QueryRootProcessor.cs b/src/EFCore/Query/QueryRootProcessor.cs index ec87a57f35d..79b6597476c 100644 --- a/src/EFCore/Query/QueryRootProcessor.cs +++ b/src/EFCore/Query/QueryRootProcessor.cs @@ -13,6 +13,9 @@ public class QueryRootProcessor : ExpressionVisitor { private readonly QueryCompilationContext _queryCompilationContext; + private static readonly bool UseOldBehavior35102 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue35102", out var enabled35102) && enabled35102; + /// /// Creates a new instance of the class with associated query provider. /// @@ -85,7 +88,21 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp private Expression VisitQueryRootCandidate(Expression expression, Type elementClrType) { - switch (expression) + var candidateExpression = expression; + + if (!UseOldBehavior35102) + { + // In case the collection was value type, in order to call methods like AsQueryable, + // we need to convert it to IEnumerable which requires boxing. + // We do that with Convert expression which we need to unwrap here. + if (expression is UnaryExpression { NodeType: ExpressionType.Convert } convertExpression + && convertExpression.Type.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + { + candidateExpression = convertExpression.Operand; + } + } + + switch (candidateExpression) { // An array containing only constants is represented as a ConstantExpression with the array as the value. // Convert that into a NewArrayExpression for use with InlineQueryRootExpression diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs index 78dd12d771c..ea203a234b2 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs @@ -613,6 +613,30 @@ WHERE ARRAY_CONTAINS(@__ints_0, c["Int"]) """ @__ints_0='[10,999]' +SELECT VALUE c +FROM root c +WHERE NOT(ARRAY_CONTAINS(@__ints_0, c["Int"])) +"""); + }); + + public override Task Parameter_collection_ImmutableArray_of_ints_Contains_int(bool async) + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + await base.Parameter_collection_ImmutableArray_of_ints_Contains_int(a); + + AssertSql( + """ +@__ints_0='[10,999]' + +SELECT VALUE c +FROM root c +WHERE ARRAY_CONTAINS(@__ints_0, c["Int"]) +""", + // + """ +@__ints_0='[10,999]' + SELECT VALUE c FROM root c WHERE NOT(ARRAY_CONTAINS(@__ints_0, c["Int"])) diff --git a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs index 2304ac3db4c..60e7cd24697 100644 --- a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.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.Immutable; + namespace Microsoft.EntityFrameworkCore.Query; public abstract class PrimitiveCollectionsQueryTestBase(TFixture fixture) : QueryTestBase(fixture) @@ -363,6 +365,20 @@ await AssertQuery( ss => ss.Set().Where(c => !ints.Contains(c.Int))); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Parameter_collection_ImmutableArray_of_ints_Contains_int(bool async) + { + var ints = ImmutableArray.Create([10, 999]); + + await AssertQuery( + async, + ss => ss.Set().Where(c => ints.Contains(c.Int))); + await AssertQuery( + async, + ss => ss.Set().Where(c => !ints.Contains(c.Int))); + } + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual async Task Parameter_collection_of_ints_Contains_nullable_int(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs index d23ea73a1ce..1041c1e10c1 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs @@ -498,6 +498,24 @@ WHERE [p].[Int] NOT IN (10, 999) """); } + public override async Task Parameter_collection_ImmutableArray_of_ints_Contains_int(bool async) + { + await base.Parameter_collection_ImmutableArray_of_ints_Contains_int(async); + + AssertSql( + """ +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Int] IN (10, 999) +""", + // + """ +SELECT [p].[Id], [p].[Bool], [p].[Bools], [p].[DateTime], [p].[DateTimes], [p].[Enum], [p].[Enums], [p].[Int], [p].[Ints], [p].[NullableInt], [p].[NullableInts], [p].[NullableString], [p].[NullableStrings], [p].[String], [p].[Strings] +FROM [PrimitiveCollectionsEntity] AS [p] +WHERE [p].[Int] NOT IN (10, 999) +"""); + } + public override async Task Parameter_collection_of_ints_Contains_nullable_int(bool async) { await base.Parameter_collection_of_ints_Contains_nullable_int(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs index a42b4ac42f7..9aea6357ba3 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs @@ -516,6 +516,34 @@ FROM OPENJSON(@__ints_0) WITH ([value] int '$') AS [i] """ @__ints_0='[10,999]' (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 [p].[Int] NOT IN ( + SELECT [i].[value] + FROM OPENJSON(@__ints_0) WITH ([value] int '$') AS [i] +) +"""); + } + + public override async Task Parameter_collection_ImmutableArray_of_ints_Contains_int(bool async) + { + await base.Parameter_collection_ImmutableArray_of_ints_Contains_int(async); + + AssertSql( + """ +@__ints_0='[10,999]' (Nullable = false) (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 [p].[Int] IN ( + SELECT [i].[value] + FROM OPENJSON(@__ints_0) WITH ([value] int '$') AS [i] +) +""", + // + """ +@__ints_0='[10,999]' (Nullable = false) (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 [p].[Int] NOT IN ( diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs index d382ca0642d..29656d04fe6 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs @@ -532,6 +532,34 @@ FROM OPENJSON(@__ints_0) WITH ([value] int '$') AS [i] """ @__ints_0='[10,999]' (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 [p].[Int] NOT IN ( + SELECT [i].[value] + FROM OPENJSON(@__ints_0) WITH ([value] int '$') AS [i] +) +"""); + } + + public override async Task Parameter_collection_ImmutableArray_of_ints_Contains_int(bool async) + { + await base.Parameter_collection_ImmutableArray_of_ints_Contains_int(async); + + AssertSql( + """ +@__ints_0='[10,999]' (Nullable = false) (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 [p].[Int] IN ( + SELECT [i].[value] + FROM OPENJSON(@__ints_0) WITH ([value] int '$') AS [i] +) +""", + // + """ +@__ints_0='[10,999]' (Nullable = false) (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 [p].[Int] NOT IN ( diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs index de05c4c3797..0708539aaa5 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs @@ -539,6 +539,34 @@ FROM OPENJSON(@__ints_0) WITH ([value] int '$') AS [i] """ @__ints_0='[10,999]' (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 [p].[Int] NOT IN ( + SELECT [i].[value] + FROM OPENJSON(@__ints_0) WITH ([value] int '$') AS [i] +) +"""); + } + + public override async Task Parameter_collection_ImmutableArray_of_ints_Contains_int(bool async) + { + await base.Parameter_collection_ImmutableArray_of_ints_Contains_int(async); + + AssertSql( + """ +@__ints_0='[10,999]' (Nullable = false) (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 [p].[Int] IN ( + SELECT [i].[value] + FROM OPENJSON(@__ints_0) WITH ([value] int '$') AS [i] +) +""", + // + """ +@__ints_0='[10,999]' (Nullable = false) (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 [p].[Int] NOT IN ( diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs index ce27814d118..58b6bb73a75 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs @@ -529,6 +529,34 @@ FROM json_each(@__ints_0) AS "i" """ @__ints_0='[10,999]' (Size = 8) +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."NullableString", "p"."NullableStrings", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE "p"."Int" NOT IN ( + SELECT "i"."value" + FROM json_each(@__ints_0) AS "i" +) +"""); + } + + public override async Task Parameter_collection_ImmutableArray_of_ints_Contains_int(bool async) + { + await base.Parameter_collection_ImmutableArray_of_ints_Contains_int(async); + + AssertSql( + """ +@__ints_0='[10,999]' (Nullable = false) (Size = 8) + +SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."NullableString", "p"."NullableStrings", "p"."String", "p"."Strings" +FROM "PrimitiveCollectionsEntity" AS "p" +WHERE "p"."Int" IN ( + SELECT "i"."value" + FROM json_each(@__ints_0) AS "i" +) +""", + // + """ +@__ints_0='[10,999]' (Nullable = false) (Size = 8) + SELECT "p"."Id", "p"."Bool", "p"."Bools", "p"."DateTime", "p"."DateTimes", "p"."Enum", "p"."Enums", "p"."Int", "p"."Ints", "p"."NullableInt", "p"."NullableInts", "p"."NullableString", "p"."NullableStrings", "p"."String", "p"."Strings" FROM "PrimitiveCollectionsEntity" AS "p" WHERE "p"."Int" NOT IN (