Skip to content

Commit 762261b

Browse files
committed
Fix handling of Contains on IReadOnlySet and custom IReadOnlyCollection.
1 parent 16c08a7 commit 762261b

File tree

9 files changed

+324
-4
lines changed

9 files changed

+324
-4
lines changed

src/EFCore/Query/Internal/QueryableMethodNormalizingExpressionVisitor.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,9 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
171171
}
172172

173173
if (method.DeclaringType is { IsGenericType: true }
174-
&& method.DeclaringType.TryGetElementType(typeof(ICollection<>)) is not null
175-
&& method.Name == nameof(ICollection<>.Contains))
174+
&& method.Name == nameof(ICollection<>.Contains)
175+
&& (method.DeclaringType.TryGetElementType(typeof(ICollection<>)) is not null
176+
|| method.DeclaringType.TryGetElementType(typeof(IReadOnlyCollection<>)) is not null))
176177
{
177178
visitedExpression = TryConvertCollectionContainsToQueryableContains(methodCallExpression);
178179
}

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2175,9 +2175,11 @@ public override Task IImmutableSet_Contains_with_parameter(bool async)
21752175

21762176
AssertSql(
21772177
"""
2178+
@ids='["ALFKI"]'
2179+
21782180
SELECT VALUE c
21792181
FROM root c
2180-
WHERE (c["id"] = "ALFKI")
2182+
WHERE ARRAY_CONTAINS(@ids, c["id"])
21812183
""");
21822184
});
21832185

@@ -2189,9 +2191,11 @@ public override Task IReadOnlySet_Contains_with_parameter(bool async)
21892191

21902192
AssertSql(
21912193
"""
2194+
@ids='["ALFKI"]'
2195+
21922196
SELECT VALUE c
21932197
FROM root c
2194-
WHERE (c["id"] = "ALFKI")
2198+
WHERE ARRAY_CONTAINS(@ids, c["id"])
21952199
""");
21962200
});
21972201

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,50 @@ WHERE ARRAY_CONTAINS(@ints, c["Int"])
597597
"""
598598
@ints='[10,999]'
599599
600+
SELECT VALUE c
601+
FROM root c
602+
WHERE NOT(ARRAY_CONTAINS(@ints, c["Int"]))
603+
""");
604+
}
605+
606+
public override async Task Parameter_collection_IReadOnlySet_of_ints_Contains_int()
607+
{
608+
await base.Parameter_collection_IReadOnlySet_of_ints_Contains_int();
609+
610+
AssertSql(
611+
"""
612+
@ints='[10,999]'
613+
614+
SELECT VALUE c
615+
FROM root c
616+
WHERE ARRAY_CONTAINS(@ints, c["Int"])
617+
""",
618+
//
619+
"""
620+
@ints='[10,999]'
621+
622+
SELECT VALUE c
623+
FROM root c
624+
WHERE NOT(ARRAY_CONTAINS(@ints, c["Int"]))
625+
""");
626+
}
627+
628+
public override async Task Parameter_collection_ReadOnlyCollectionWithContains_of_ints_Contains_int()
629+
{
630+
await base.Parameter_collection_ReadOnlyCollectionWithContains_of_ints_Contains_int();
631+
632+
AssertSql(
633+
"""
634+
@ints='[10,999]'
635+
636+
SELECT VALUE c
637+
FROM root c
638+
WHERE ARRAY_CONTAINS(@ints, c["Int"])
639+
""",
640+
//
641+
"""
642+
@ints='[10,999]'
643+
600644
SELECT VALUE c
601645
FROM root c
602646
WHERE NOT(ARRAY_CONTAINS(@ints, c["Int"]))

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
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;
45
using System.Collections.Frozen;
56
using System.Collections.Immutable;
67

@@ -294,6 +295,25 @@ public virtual async Task Parameter_collection_ImmutableArray_of_ints_Contains_i
294295
await AssertQuery(ss => ss.Set<PrimitiveCollectionsEntity>().Where(c => !ints.Contains(c.Int)));
295296
}
296297

298+
[ConditionalFact]
299+
public virtual async Task Parameter_collection_IReadOnlySet_of_ints_Contains_int()
300+
{
301+
// IReadOnlySet<T> has Contains defined directly on itself
302+
IReadOnlySet<int> ints = new HashSet<int> { 10, 999 };
303+
304+
await AssertQuery(ss => ss.Set<PrimitiveCollectionsEntity>().Where(c => ints.Contains(c.Int)));
305+
await AssertQuery(ss => ss.Set<PrimitiveCollectionsEntity>().Where(c => !ints.Contains(c.Int)));
306+
}
307+
308+
[ConditionalFact]
309+
public virtual async Task Parameter_collection_ReadOnlyCollectionWithContains_of_ints_Contains_int()
310+
{
311+
var ints = new ReadOnlyCollectionWithContains<int>(10, 999);
312+
313+
await AssertQuery(ss => ss.Set<PrimitiveCollectionsEntity>().Where(c => ints.Contains(c.Int)));
314+
await AssertQuery(ss => ss.Set<PrimitiveCollectionsEntity>().Where(c => !ints.Contains(c.Int)));
315+
}
316+
297317
[ConditionalFact]
298318
public virtual async Task Parameter_collection_of_ints_Contains_nullable_int()
299319
{
@@ -1835,3 +1855,14 @@ private static IReadOnlyList<PrimitiveCollectionsEntity> CreatePrimitiveArrayEnt
18351855
};
18361856
}
18371857
}
1858+
1859+
// Keep it outside so it does not inherit the TFixture.
1860+
internal class ReadOnlyCollectionWithContains<T>(params T[] items) : IReadOnlyCollection<T>
1861+
{
1862+
public int Count => items.Length;
1863+
1864+
public bool Contains(T item) => items.Contains(item);
1865+
1866+
public IEnumerator<T> GetEnumerator() => items.AsEnumerable().GetEnumerator();
1867+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
1868+
}

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,54 @@ WHERE [p].[Int] IN (@ints1, @ints2)
571571
@ints1='10'
572572
@ints2='999'
573573
574+
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]
575+
FROM [PrimitiveCollectionsEntity] AS [p]
576+
WHERE [p].[Int] NOT IN (@ints1, @ints2)
577+
""");
578+
}
579+
580+
public override async Task Parameter_collection_IReadOnlySet_of_ints_Contains_int()
581+
{
582+
await base.Parameter_collection_IReadOnlySet_of_ints_Contains_int();
583+
584+
AssertSql(
585+
"""
586+
@ints1='10'
587+
@ints2='999'
588+
589+
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]
590+
FROM [PrimitiveCollectionsEntity] AS [p]
591+
WHERE [p].[Int] IN (@ints1, @ints2)
592+
""",
593+
//
594+
"""
595+
@ints1='10'
596+
@ints2='999'
597+
598+
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]
599+
FROM [PrimitiveCollectionsEntity] AS [p]
600+
WHERE [p].[Int] NOT IN (@ints1, @ints2)
601+
""");
602+
}
603+
604+
public override async Task Parameter_collection_ReadOnlyCollectionWithContains_of_ints_Contains_int()
605+
{
606+
await base.Parameter_collection_ReadOnlyCollectionWithContains_of_ints_Contains_int();
607+
608+
AssertSql(
609+
"""
610+
@ints1='10'
611+
@ints2='999'
612+
613+
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]
614+
FROM [PrimitiveCollectionsEntity] AS [p]
615+
WHERE [p].[Int] IN (@ints1, @ints2)
616+
""",
617+
//
618+
"""
619+
@ints1='10'
620+
@ints2='999'
621+
574622
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]
575623
FROM [PrimitiveCollectionsEntity] AS [p]
576624
WHERE [p].[Int] NOT IN (@ints1, @ints2)

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,54 @@ WHERE [p].[Int] IN (@ints1, @ints2)
579579
@ints1='10'
580580
@ints2='999'
581581
582+
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]
583+
FROM [PrimitiveCollectionsEntity] AS [p]
584+
WHERE [p].[Int] NOT IN (@ints1, @ints2)
585+
""");
586+
}
587+
588+
public override async Task Parameter_collection_IReadOnlySet_of_ints_Contains_int()
589+
{
590+
await base.Parameter_collection_IReadOnlySet_of_ints_Contains_int();
591+
592+
AssertSql(
593+
"""
594+
@ints1='10'
595+
@ints2='999'
596+
597+
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]
598+
FROM [PrimitiveCollectionsEntity] AS [p]
599+
WHERE [p].[Int] IN (@ints1, @ints2)
600+
""",
601+
//
602+
"""
603+
@ints1='10'
604+
@ints2='999'
605+
606+
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]
607+
FROM [PrimitiveCollectionsEntity] AS [p]
608+
WHERE [p].[Int] NOT IN (@ints1, @ints2)
609+
""");
610+
}
611+
612+
public override async Task Parameter_collection_ReadOnlyCollectionWithContains_of_ints_Contains_int()
613+
{
614+
await base.Parameter_collection_ReadOnlyCollectionWithContains_of_ints_Contains_int();
615+
616+
AssertSql(
617+
"""
618+
@ints1='10'
619+
@ints2='999'
620+
621+
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]
622+
FROM [PrimitiveCollectionsEntity] AS [p]
623+
WHERE [p].[Int] IN (@ints1, @ints2)
624+
""",
625+
//
626+
"""
627+
@ints1='10'
628+
@ints2='999'
629+
582630
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]
583631
FROM [PrimitiveCollectionsEntity] AS [p]
584632
WHERE [p].[Int] NOT IN (@ints1, @ints2)

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,54 @@ WHERE [p].[Int] IN (@ints1, @ints2)
723723
@ints1='10'
724724
@ints2='999'
725725
726+
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]
727+
FROM [PrimitiveCollectionsEntity] AS [p]
728+
WHERE [p].[Int] NOT IN (@ints1, @ints2)
729+
""");
730+
}
731+
732+
public override async Task Parameter_collection_IReadOnlySet_of_ints_Contains_int()
733+
{
734+
await base.Parameter_collection_IReadOnlySet_of_ints_Contains_int();
735+
736+
AssertSql(
737+
"""
738+
@ints1='10'
739+
@ints2='999'
740+
741+
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]
742+
FROM [PrimitiveCollectionsEntity] AS [p]
743+
WHERE [p].[Int] IN (@ints1, @ints2)
744+
""",
745+
//
746+
"""
747+
@ints1='10'
748+
@ints2='999'
749+
750+
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]
751+
FROM [PrimitiveCollectionsEntity] AS [p]
752+
WHERE [p].[Int] NOT IN (@ints1, @ints2)
753+
""");
754+
}
755+
756+
public override async Task Parameter_collection_ReadOnlyCollectionWithContains_of_ints_Contains_int()
757+
{
758+
await base.Parameter_collection_ReadOnlyCollectionWithContains_of_ints_Contains_int();
759+
760+
AssertSql(
761+
"""
762+
@ints1='10'
763+
@ints2='999'
764+
765+
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]
766+
FROM [PrimitiveCollectionsEntity] AS [p]
767+
WHERE [p].[Int] IN (@ints1, @ints2)
768+
""",
769+
//
770+
"""
771+
@ints1='10'
772+
@ints2='999'
773+
726774
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]
727775
FROM [PrimitiveCollectionsEntity] AS [p]
728776
WHERE [p].[Int] NOT IN (@ints1, @ints2)

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,54 @@ WHERE [p].[Int] IN (@ints1, @ints2)
602602
@ints1='10'
603603
@ints2='999'
604604
605+
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]
606+
FROM [PrimitiveCollectionsEntity] AS [p]
607+
WHERE [p].[Int] NOT IN (@ints1, @ints2)
608+
""");
609+
}
610+
611+
public override async Task Parameter_collection_IReadOnlySet_of_ints_Contains_int()
612+
{
613+
await base.Parameter_collection_IReadOnlySet_of_ints_Contains_int();
614+
615+
AssertSql(
616+
"""
617+
@ints1='10'
618+
@ints2='999'
619+
620+
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]
621+
FROM [PrimitiveCollectionsEntity] AS [p]
622+
WHERE [p].[Int] IN (@ints1, @ints2)
623+
""",
624+
//
625+
"""
626+
@ints1='10'
627+
@ints2='999'
628+
629+
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]
630+
FROM [PrimitiveCollectionsEntity] AS [p]
631+
WHERE [p].[Int] NOT IN (@ints1, @ints2)
632+
""");
633+
}
634+
635+
public override async Task Parameter_collection_ReadOnlyCollectionWithContains_of_ints_Contains_int()
636+
{
637+
await base.Parameter_collection_ReadOnlyCollectionWithContains_of_ints_Contains_int();
638+
639+
AssertSql(
640+
"""
641+
@ints1='10'
642+
@ints2='999'
643+
644+
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]
645+
FROM [PrimitiveCollectionsEntity] AS [p]
646+
WHERE [p].[Int] IN (@ints1, @ints2)
647+
""",
648+
//
649+
"""
650+
@ints1='10'
651+
@ints2='999'
652+
605653
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]
606654
FROM [PrimitiveCollectionsEntity] AS [p]
607655
WHERE [p].[Int] NOT IN (@ints1, @ints2)

0 commit comments

Comments
 (0)