Skip to content

Commit 6489581

Browse files
authored
[release/9.0] Fix Contains on ImmutableArray (#35251)
1 parent 507152b commit 6489581

File tree

9 files changed

+196
-2
lines changed

9 files changed

+196
-2
lines changed

src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ public class QueryableMethodNormalizingExpressionVisitor : ExpressionVisitor
2020
private readonly SelectManyVerifyingExpressionVisitor _selectManyVerifyingExpressionVisitor = new();
2121
private readonly GroupJoinConvertingExpressionVisitor _groupJoinConvertingExpressionVisitor = new();
2222

23+
private static readonly bool UseOldBehavior35102 =
24+
AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue35102", out var enabled35102) && enabled35102;
25+
2326
/// <summary>
2427
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
2528
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -489,12 +492,16 @@ private Expression TryConvertCollectionContainsToQueryableContains(MethodCallExp
489492

490493
var sourceType = methodCallExpression.Method.DeclaringType!.GetGenericArguments()[0];
491494

495+
var objectExpression = methodCallExpression.Object!.Type.IsValueType && !UseOldBehavior35102
496+
? Expression.Convert(methodCallExpression.Object!, typeof(IEnumerable<>).MakeGenericType(sourceType))
497+
: methodCallExpression.Object!;
498+
492499
return VisitMethodCall(
493500
Expression.Call(
494501
QueryableMethods.Contains.MakeGenericMethod(sourceType),
495502
Expression.Call(
496503
QueryableMethods.AsQueryable.MakeGenericMethod(sourceType),
497-
methodCallExpression.Object!),
504+
objectExpression),
498505
methodCallExpression.Arguments[0]));
499506
}
500507

src/EFCore/Query/QueryRootProcessor.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ public class QueryRootProcessor : ExpressionVisitor
1313
{
1414
private readonly QueryCompilationContext _queryCompilationContext;
1515

16+
private static readonly bool UseOldBehavior35102 =
17+
AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue35102", out var enabled35102) && enabled35102;
18+
1619
/// <summary>
1720
/// Creates a new instance of the <see cref="QueryRootProcessor" /> class with associated query provider.
1821
/// </summary>
@@ -85,7 +88,21 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
8588

8689
private Expression VisitQueryRootCandidate(Expression expression, Type elementClrType)
8790
{
88-
switch (expression)
91+
var candidateExpression = expression;
92+
93+
if (!UseOldBehavior35102)
94+
{
95+
// In case the collection was value type, in order to call methods like AsQueryable,
96+
// we need to convert it to IEnumerable<T> which requires boxing.
97+
// We do that with Convert expression which we need to unwrap here.
98+
if (expression is UnaryExpression { NodeType: ExpressionType.Convert } convertExpression
99+
&& convertExpression.Type.GetGenericTypeDefinition() == typeof(IEnumerable<>))
100+
{
101+
candidateExpression = convertExpression.Operand;
102+
}
103+
}
104+
105+
switch (candidateExpression)
89106
{
90107
// An array containing only constants is represented as a ConstantExpression with the array as the value.
91108
// Convert that into a NewArrayExpression for use with InlineQueryRootExpression

test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,30 @@ WHERE ARRAY_CONTAINS(@__ints_0, c["Int"])
613613
"""
614614
@__ints_0='[10,999]'
615615
616+
SELECT VALUE c
617+
FROM root c
618+
WHERE NOT(ARRAY_CONTAINS(@__ints_0, c["Int"]))
619+
""");
620+
});
621+
622+
public override Task Parameter_collection_ImmutableArray_of_ints_Contains_int(bool async)
623+
=> CosmosTestHelpers.Instance.NoSyncTest(
624+
async, async a =>
625+
{
626+
await base.Parameter_collection_ImmutableArray_of_ints_Contains_int(a);
627+
628+
AssertSql(
629+
"""
630+
@__ints_0='[10,999]'
631+
632+
SELECT VALUE c
633+
FROM root c
634+
WHERE ARRAY_CONTAINS(@__ints_0, c["Int"])
635+
""",
636+
//
637+
"""
638+
@__ints_0='[10,999]'
639+
616640
SELECT VALUE c
617641
FROM root c
618642
WHERE NOT(ARRAY_CONTAINS(@__ints_0, c["Int"]))

test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Collections.Immutable;
5+
46
namespace Microsoft.EntityFrameworkCore.Query;
57

68
public abstract class PrimitiveCollectionsQueryTestBase<TFixture>(TFixture fixture) : QueryTestBase<TFixture>(fixture)
@@ -363,6 +365,20 @@ await AssertQuery(
363365
ss => ss.Set<PrimitiveCollectionsEntity>().Where(c => !ints.Contains(c.Int)));
364366
}
365367

368+
[ConditionalTheory]
369+
[MemberData(nameof(IsAsyncData))]
370+
public virtual async Task Parameter_collection_ImmutableArray_of_ints_Contains_int(bool async)
371+
{
372+
var ints = ImmutableArray.Create([10, 999]);
373+
374+
await AssertQuery(
375+
async,
376+
ss => ss.Set<PrimitiveCollectionsEntity>().Where(c => ints.Contains(c.Int)));
377+
await AssertQuery(
378+
async,
379+
ss => ss.Set<PrimitiveCollectionsEntity>().Where(c => !ints.Contains(c.Int)));
380+
}
381+
366382
[ConditionalTheory]
367383
[MemberData(nameof(IsAsyncData))]
368384
public virtual async Task Parameter_collection_of_ints_Contains_nullable_int(bool async)

test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,24 @@ WHERE [p].[Int] NOT IN (10, 999)
498498
""");
499499
}
500500

501+
public override async Task Parameter_collection_ImmutableArray_of_ints_Contains_int(bool async)
502+
{
503+
await base.Parameter_collection_ImmutableArray_of_ints_Contains_int(async);
504+
505+
AssertSql(
506+
"""
507+
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]
508+
FROM [PrimitiveCollectionsEntity] AS [p]
509+
WHERE [p].[Int] IN (10, 999)
510+
""",
511+
//
512+
"""
513+
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]
514+
FROM [PrimitiveCollectionsEntity] AS [p]
515+
WHERE [p].[Int] NOT IN (10, 999)
516+
""");
517+
}
518+
501519
public override async Task Parameter_collection_of_ints_Contains_nullable_int(bool async)
502520
{
503521
await base.Parameter_collection_of_ints_Contains_nullable_int(async);

test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,34 @@ FROM OPENJSON(@__ints_0) WITH ([value] int '$') AS [i]
516516
"""
517517
@__ints_0='[10,999]' (Size = 4000)
518518
519+
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]
520+
FROM [PrimitiveCollectionsEntity] AS [p]
521+
WHERE [p].[Int] NOT IN (
522+
SELECT [i].[value]
523+
FROM OPENJSON(@__ints_0) WITH ([value] int '$') AS [i]
524+
)
525+
""");
526+
}
527+
528+
public override async Task Parameter_collection_ImmutableArray_of_ints_Contains_int(bool async)
529+
{
530+
await base.Parameter_collection_ImmutableArray_of_ints_Contains_int(async);
531+
532+
AssertSql(
533+
"""
534+
@__ints_0='[10,999]' (Nullable = false) (Size = 4000)
535+
536+
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]
537+
FROM [PrimitiveCollectionsEntity] AS [p]
538+
WHERE [p].[Int] IN (
539+
SELECT [i].[value]
540+
FROM OPENJSON(@__ints_0) WITH ([value] int '$') AS [i]
541+
)
542+
""",
543+
//
544+
"""
545+
@__ints_0='[10,999]' (Nullable = false) (Size = 4000)
546+
519547
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]
520548
FROM [PrimitiveCollectionsEntity] AS [p]
521549
WHERE [p].[Int] NOT IN (

test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,34 @@ FROM OPENJSON(@__ints_0) WITH ([value] int '$') AS [i]
532532
"""
533533
@__ints_0='[10,999]' (Size = 4000)
534534
535+
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]
536+
FROM [PrimitiveCollectionsEntity] AS [p]
537+
WHERE [p].[Int] NOT IN (
538+
SELECT [i].[value]
539+
FROM OPENJSON(@__ints_0) WITH ([value] int '$') AS [i]
540+
)
541+
""");
542+
}
543+
544+
public override async Task Parameter_collection_ImmutableArray_of_ints_Contains_int(bool async)
545+
{
546+
await base.Parameter_collection_ImmutableArray_of_ints_Contains_int(async);
547+
548+
AssertSql(
549+
"""
550+
@__ints_0='[10,999]' (Nullable = false) (Size = 4000)
551+
552+
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]
553+
FROM [PrimitiveCollectionsEntity] AS [p]
554+
WHERE [p].[Int] IN (
555+
SELECT [i].[value]
556+
FROM OPENJSON(@__ints_0) WITH ([value] int '$') AS [i]
557+
)
558+
""",
559+
//
560+
"""
561+
@__ints_0='[10,999]' (Nullable = false) (Size = 4000)
562+
535563
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]
536564
FROM [PrimitiveCollectionsEntity] AS [p]
537565
WHERE [p].[Int] NOT IN (

test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,34 @@ FROM OPENJSON(@__ints_0) WITH ([value] int '$') AS [i]
539539
"""
540540
@__ints_0='[10,999]' (Size = 4000)
541541
542+
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]
543+
FROM [PrimitiveCollectionsEntity] AS [p]
544+
WHERE [p].[Int] NOT IN (
545+
SELECT [i].[value]
546+
FROM OPENJSON(@__ints_0) WITH ([value] int '$') AS [i]
547+
)
548+
""");
549+
}
550+
551+
public override async Task Parameter_collection_ImmutableArray_of_ints_Contains_int(bool async)
552+
{
553+
await base.Parameter_collection_ImmutableArray_of_ints_Contains_int(async);
554+
555+
AssertSql(
556+
"""
557+
@__ints_0='[10,999]' (Nullable = false) (Size = 4000)
558+
559+
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]
560+
FROM [PrimitiveCollectionsEntity] AS [p]
561+
WHERE [p].[Int] IN (
562+
SELECT [i].[value]
563+
FROM OPENJSON(@__ints_0) WITH ([value] int '$') AS [i]
564+
)
565+
""",
566+
//
567+
"""
568+
@__ints_0='[10,999]' (Nullable = false) (Size = 4000)
569+
542570
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]
543571
FROM [PrimitiveCollectionsEntity] AS [p]
544572
WHERE [p].[Int] NOT IN (

test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,34 @@ FROM json_each(@__ints_0) AS "i"
529529
"""
530530
@__ints_0='[10,999]' (Size = 8)
531531
532+
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"
533+
FROM "PrimitiveCollectionsEntity" AS "p"
534+
WHERE "p"."Int" NOT IN (
535+
SELECT "i"."value"
536+
FROM json_each(@__ints_0) AS "i"
537+
)
538+
""");
539+
}
540+
541+
public override async Task Parameter_collection_ImmutableArray_of_ints_Contains_int(bool async)
542+
{
543+
await base.Parameter_collection_ImmutableArray_of_ints_Contains_int(async);
544+
545+
AssertSql(
546+
"""
547+
@__ints_0='[10,999]' (Nullable = false) (Size = 8)
548+
549+
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"
550+
FROM "PrimitiveCollectionsEntity" AS "p"
551+
WHERE "p"."Int" IN (
552+
SELECT "i"."value"
553+
FROM json_each(@__ints_0) AS "i"
554+
)
555+
""",
556+
//
557+
"""
558+
@__ints_0='[10,999]' (Nullable = false) (Size = 8)
559+
532560
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"
533561
FROM "PrimitiveCollectionsEntity" AS "p"
534562
WHERE "p"."Int" NOT IN (

0 commit comments

Comments
 (0)