Skip to content

Commit 14e6557

Browse files
committed
Fix Contains on ImmutableArray (dotnet#35247)
1 parent 1c0ef32 commit 14e6557

File tree

9 files changed

+187
-2
lines changed

9 files changed

+187
-2
lines changed

src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,12 +489,16 @@ private Expression TryConvertCollectionContainsToQueryableContains(MethodCallExp
489489

490490
var sourceType = methodCallExpression.Method.DeclaringType!.GetGenericArguments()[0];
491491

492+
var objectExpression = methodCallExpression.Object!.Type.IsValueType
493+
? Expression.Convert(methodCallExpression.Object!, typeof(IEnumerable<>).MakeGenericType(sourceType))
494+
: methodCallExpression.Object!;
495+
492496
return VisitMethodCall(
493497
Expression.Call(
494498
QueryableMethods.Contains.MakeGenericMethod(sourceType),
495499
Expression.Call(
496500
QueryableMethods.AsQueryable.MakeGenericMethod(sourceType),
497-
methodCallExpression.Object!),
501+
objectExpression),
498502
methodCallExpression.Arguments[0]));
499503
}
500504

src/EFCore/Query/QueryRootProcessor.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,18 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
8585

8686
private Expression VisitQueryRootCandidate(Expression expression, Type elementClrType)
8787
{
88-
switch (expression)
88+
var candidateExpression = expression;
89+
90+
// In case the collection was value type, in order to call methods like AsQueryable,
91+
// we need to convert it to IEnumerable<T> which requires boxing.
92+
// We do that with Convert expression which we need to unwrap here.
93+
if (expression is UnaryExpression { NodeType: ExpressionType.Convert } convertExpression
94+
&& convertExpression.Type.GetGenericTypeDefinition() == typeof(IEnumerable<>))
95+
{
96+
candidateExpression = convertExpression.Operand;
97+
}
98+
99+
switch (candidateExpression)
89100
{
90101
// An array containing only constants is represented as a ConstantExpression with the array as the value.
91102
// 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
@@ -616,6 +616,30 @@ WHERE ARRAY_CONTAINS(@__ints_0, c["Int"])
616616
SELECT VALUE c
617617
FROM root c
618618
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='[10,999]'
631+
632+
SELECT VALUE c
633+
FROM root c
634+
WHERE ARRAY_CONTAINS(@ints, c["Int"])
635+
""",
636+
//
637+
"""
638+
@ints='[10,999]'
639+
640+
SELECT VALUE c
641+
FROM root c
642+
WHERE NOT(ARRAY_CONTAINS(@ints, c["Int"]))
619643
""");
620644
});
621645

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].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId]
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].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId]
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
@@ -525,6 +525,34 @@ FROM OPENJSON(@__ints_0) WITH ([value] int '$') AS [i]
525525
""");
526526
}
527527

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='[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].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId]
537+
FROM [PrimitiveCollectionsEntity] AS [p]
538+
WHERE [p].[Int] IN (
539+
SELECT [i].[value]
540+
FROM OPENJSON(@ints) WITH ([value] int '$') AS [i]
541+
)
542+
""",
543+
//
544+
"""
545+
@ints='[10,999]' (Nullable = false) (Size = 4000)
546+
547+
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].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId]
548+
FROM [PrimitiveCollectionsEntity] AS [p]
549+
WHERE [p].[Int] NOT IN (
550+
SELECT [i].[value]
551+
FROM OPENJSON(@ints) WITH ([value] int '$') AS [i]
552+
)
553+
""");
554+
}
555+
528556
public override async Task Parameter_collection_of_ints_Contains_nullable_int(bool async)
529557
{
530558
await base.Parameter_collection_of_ints_Contains_nullable_int(async);

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,34 @@ FROM OPENJSON(@__ints_0) WITH ([value] int '$') AS [i]
541541
""");
542542
}
543543

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='[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) WITH ([value] int '$') AS [i]
557+
)
558+
""",
559+
//
560+
"""
561+
@ints='[10,999]' (Nullable = false) (Size = 4000)
562+
563+
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]
564+
FROM [PrimitiveCollectionsEntity] AS [p]
565+
WHERE [p].[Int] NOT IN (
566+
SELECT [i].[value]
567+
FROM OPENJSON(@ints) WITH ([value] int '$') AS [i]
568+
)
569+
""");
570+
}
571+
544572
public override async Task Parameter_collection_of_ints_Contains_nullable_int(bool async)
545573
{
546574
await base.Parameter_collection_of_ints_Contains_nullable_int(async);

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,34 @@ FROM OPENJSON(@__ints_0) WITH ([value] int '$') AS [i]
548548
""");
549549
}
550550

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='[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].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId]
560+
FROM [PrimitiveCollectionsEntity] AS [p]
561+
WHERE [p].[Int] IN (
562+
SELECT [i].[value]
563+
FROM OPENJSON(@ints) WITH ([value] int '$') AS [i]
564+
)
565+
""",
566+
//
567+
"""
568+
@ints='[10,999]' (Nullable = false) (Size = 4000)
569+
570+
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].[NullableWrappedId], [p].[NullableWrappedIdWithNullableComparer], [p].[String], [p].[Strings], [p].[WrappedId]
571+
FROM [PrimitiveCollectionsEntity] AS [p]
572+
WHERE [p].[Int] NOT IN (
573+
SELECT [i].[value]
574+
FROM OPENJSON(@ints) WITH ([value] int '$') AS [i]
575+
)
576+
""");
577+
}
578+
551579
public override async Task Parameter_collection_of_ints_Contains_nullable_int(bool async)
552580
{
553581
await base.Parameter_collection_of_ints_Contains_nullable_int(async);

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,34 @@ FROM json_each(@__ints_0) AS "i"
538538
""");
539539
}
540540

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='[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"."NullableWrappedId", "p"."NullableWrappedIdWithNullableComparer", "p"."String", "p"."Strings", "p"."WrappedId"
550+
FROM "PrimitiveCollectionsEntity" AS "p"
551+
WHERE "p"."Int" IN (
552+
SELECT "i"."value"
553+
FROM json_each(@ints) AS "i"
554+
)
555+
""",
556+
//
557+
"""
558+
@ints='[10,999]' (Nullable = false) (Size = 8)
559+
560+
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"."NullableWrappedId", "p"."NullableWrappedIdWithNullableComparer", "p"."String", "p"."Strings", "p"."WrappedId"
561+
FROM "PrimitiveCollectionsEntity" AS "p"
562+
WHERE "p"."Int" NOT IN (
563+
SELECT "i"."value"
564+
FROM json_each(@ints) AS "i"
565+
)
566+
""");
567+
}
568+
541569
public override async Task Parameter_collection_of_ints_Contains_nullable_int(bool async)
542570
{
543571
await base.Parameter_collection_of_ints_Contains_nullable_int(async);

0 commit comments

Comments
 (0)