diff --git a/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs b/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs index eb02e70da83..95fa51cb9be 100644 --- a/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs +++ b/src/EFCore/Query/Internal/ExpressionTreeFuncletizer.cs @@ -115,6 +115,9 @@ public class ExpressionTreeFuncletizer : ExpressionVisitor private static readonly bool UseOldBehavior35100 = AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue35100", out var enabled35100) && enabled35100; + private static readonly bool UseOldBehavior37176 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue37176", out var enabled37176) && enabled37176; + private static readonly MethodInfo ReadOnlyCollectionIndexerGetter = typeof(ReadOnlyCollection).GetProperties() .Single(p => p.GetIndexParameters() is { Length: 1 } indexParameters && indexParameters[0].ParameterType == typeof(int)).GetMethod!; @@ -985,7 +988,9 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCall) switch (method.Name) { case nameof(MemoryExtensions.Contains) - when methodCall.Arguments is [var arg0, var arg1] && TryUnwrapSpanImplicitCast(arg0, out var unwrappedArg0): + when UseOldBehavior37176 + && methodCall.Arguments is [var arg0, var arg1] + && TryUnwrapSpanImplicitCast(arg0, out var unwrappedArg0): { return Visit( Call( @@ -993,6 +998,22 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCall) unwrappedArg0, arg1)); } + // In .NET 10, MemoryExtensions.Contains has an overload that accepts a third, optional comparer, in addition to the older + // overload that accepts two parameters only. + case nameof(MemoryExtensions.Contains) + when !UseOldBehavior37176 + && methodCall.Arguments is [var spanArg, var valueArg, ..] + && (methodCall.Arguments.Count is 2 + || methodCall.Arguments.Count is 3 + && methodCall.Arguments[2] is ConstantExpression { Value: null }) + && TryUnwrapSpanImplicitCast(spanArg, out var unwrappedSpanArg): + { + return Visit( + Call( + EnumerableMethods.Contains.MakeGenericMethod(method.GetGenericArguments()[0]), + unwrappedSpanArg, valueArg)); + } + case nameof(MemoryExtensions.SequenceEqual) when methodCall.Arguments is [var arg0, var arg1] && TryUnwrapSpanImplicitCast(arg0, out var unwrappedArg0) @@ -1005,20 +1026,37 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCall) static bool TryUnwrapSpanImplicitCast(Expression expression, [NotNullWhen(true)] out Expression? result) { - if (expression is MethodCallExpression + switch (expression) + { + // With newer versions of the SDK, the implicit cast is represented as a MethodCallExpression; + // with older versions, it's a Convert node. + case MethodCallExpression { Method: { Name: "op_Implicit", DeclaringType: { IsGenericType: true } implicitCastDeclaringType }, Arguments: [var unwrapped] + } when implicitCastDeclaringType.GetGenericTypeDefinition() is var genericTypeDefinition + && (genericTypeDefinition == typeof(Span<>) || genericTypeDefinition == typeof(ReadOnlySpan<>)): + { + result = unwrapped; + return true; } - && implicitCastDeclaringType.GetGenericTypeDefinition() is var genericTypeDefinition - && (genericTypeDefinition == typeof(Span<>) || genericTypeDefinition == typeof(ReadOnlySpan<>))) - { - result = unwrapped; - return true; - } - result = null; - return false; + case UnaryExpression + { + NodeType: ExpressionType.Convert, + Operand: var unwrapped, + Type: { IsGenericType: true } convertType + } when !UseOldBehavior37176 && convertType.GetGenericTypeDefinition() is var genericTypeDefinition + && (genericTypeDefinition == typeof(Span<>) || genericTypeDefinition == typeof(ReadOnlySpan<>)): + { + result = unwrapped; + return true; + } + + default: + result = null; + return false; + } } } diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs index ea203a234b2..b8f411bcbb7 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs @@ -997,6 +997,50 @@ FROM root c """); }); + public override Task Contains_on_Enumerable(bool async) + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + await base.Contains_on_Enumerable(a); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE c["Int"] IN (10, 999) +"""); + }); + + public override Task Contains_on_MemoryExtensions(bool async) + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + await base.Contains_on_MemoryExtensions(a); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE c["Int"] IN (10, 999) +"""); + }); + +#if NET10_0_OR_GREATER + public override Task Contains_with_MemoryExtensions_with_null_comparer(bool async) + => CosmosTestHelpers.Instance.NoSyncTest( + async, async a => + { + await base.Contains_with_MemoryExtensions_with_null_comparer(a); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE c["Int"] IN (10, 999) +"""); + }); +#endif + public override Task Column_collection_Length(bool async) => CosmosTestHelpers.Instance.NoSyncTest( async, async a => diff --git a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs index 60e7cd24697..10bc9225e49 100644 --- a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs @@ -607,6 +607,34 @@ public virtual Task Column_collection_of_bools_Contains(bool async) async, ss => ss.Set().Where(c => c.Bools.Contains(true))); + // C# 14 first-class spans caused MemoryExtensions.Contains to get resolved instead of Enumerable.Contains. + // The following tests that the various overloads are all supported. + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Contains_on_Enumerable(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => Enumerable.Contains(new[] { 10, 999 }, c.Int))); + + // C# 14 first-class spans caused MemoryExtensions.Contains to get resolved instead of Enumerable.Contains. + // The following tests that the various overloads are all supported. + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Contains_on_MemoryExtensions(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => MemoryExtensions.Contains(new[] { 10, 999 }, c.Int))); + + // Note that we don't test EF 8/9 with .NET 10; this test is here for completeness/documentation purposes. +#if NET10_0_OR_GREATER + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Contains_with_MemoryExtensions_with_null_comparer(bool async) + => AssertQuery( + async, + ss => ss.Set().Where(c => MemoryExtensions.Contains(new[] { 10, 999 }, c.Int, comparer: null))); +#endif + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Column_collection_Count_method(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs index 1041c1e10c1..a492f97fd88 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs @@ -761,6 +761,45 @@ await context.Database.SqlQuery($"SELECT [Bools] AS [Value] FROM [Primit .SingleAsync()); } + public override async Task Contains_on_Enumerable(bool async) + { + await base.Contains_on_Enumerable(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) +"""); + } + + + public override async Task Contains_on_MemoryExtensions(bool async) + { + await base.Contains_on_MemoryExtensions(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) +"""); + } + +#if NET10_0_OR_GREATER + public override async Task Contains_with_MemoryExtensions_with_null_comparer(bool async) + { + await base.Contains_with_MemoryExtensions_with_null_comparer(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) +"""); + } +#endif + public override Task Column_collection_Count_method(bool async) => AssertCompatibilityLevelTooLow(() => base.Column_collection_Count_method(async)); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs index 9aea6357ba3..186799afd4e 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServer160Test.cs @@ -955,6 +955,45 @@ await context.Database.SqlQuery($"SELECT [Bools] AS [Value] FROM [Primit .SingleAsync()); } + public override async Task Contains_on_Enumerable(bool async) + { + await base.Contains_on_Enumerable(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) +"""); + } + + + public override async Task Contains_on_MemoryExtensions(bool async) + { + await base.Contains_on_MemoryExtensions(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) +"""); + } + +#if NET10_0_OR_GREATER + public override async Task Contains_with_MemoryExtensions_with_null_comparer(bool async) + { + await base.Contains_with_MemoryExtensions_with_null_comparer(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) +"""); + } +#endif + public override async Task Column_collection_Count_method(bool async) { await base.Column_collection_Count_method(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs index 29656d04fe6..0e27003a1e2 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerJsonTypeTest.cs @@ -929,6 +929,45 @@ await context.Database.SqlQuery($"SELECT [Bools] AS [Value] FROM [Primit .SingleAsync()); } + public override async Task Contains_on_Enumerable(bool async) + { + await base.Contains_on_Enumerable(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) +"""); + } + + + public override async Task Contains_on_MemoryExtensions(bool async) + { + await base.Contains_on_MemoryExtensions(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) +"""); + } + +#if NET10_0_OR_GREATER + public override async Task Contains_with_MemoryExtensions_with_null_comparer(bool async) + { + await base.Contains_with_MemoryExtensions_with_null_comparer(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) +"""); + } +#endif + public override async Task Column_collection_Count_method(bool async) { await base.Column_collection_Count_method(async); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs index 0708539aaa5..ed20eb7db20 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs @@ -978,6 +978,45 @@ await context.Database.SqlQuery($"SELECT [Bools] AS [Value] FROM [Primit .SingleAsync()); } + public override async Task Contains_on_Enumerable(bool async) + { + await base.Contains_on_Enumerable(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) +"""); + } + + + public override async Task Contains_on_MemoryExtensions(bool async) + { + await base.Contains_on_MemoryExtensions(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) +"""); + } + +#if NET10_0_OR_GREATER + public override async Task Contains_with_MemoryExtensions_with_null_comparer(bool async) + { + await base.Contains_with_MemoryExtensions_with_null_comparer(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) +"""); + } +#endif + public override async Task Column_collection_Count_method(bool async) { await base.Column_collection_Count_method(async); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs index 58b6bb73a75..68a914a22c5 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs @@ -957,6 +957,45 @@ FROM json_each("p"."Bools") AS "b" """); } + public override async Task Contains_on_Enumerable(bool async) + { + await base.Contains_on_Enumerable(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) +"""); + } + + + public override async Task Contains_on_MemoryExtensions(bool async) + { + await base.Contains_on_MemoryExtensions(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) +"""); + } + +#if NET10_0_OR_GREATER + public override async Task Contains_with_MemoryExtensions_with_null_comparer(bool async) + { + await base.Contains_with_MemoryExtensions_with_null_comparer(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) +"""); + } +#endif + public override async Task Column_collection_Count_method(bool async) { await base.Column_collection_Count_method(async);