diff --git a/src/EFCore/Query/Internal/ParameterExtractingExpressionVisitor.cs b/src/EFCore/Query/Internal/ParameterExtractingExpressionVisitor.cs index 410f2dd2839..ee3b82baf90 100644 --- a/src/EFCore/Query/Internal/ParameterExtractingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/ParameterExtractingExpressionVisitor.cs @@ -34,6 +34,9 @@ public class ParameterExtractingExpressionVisitor : 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; + /// /// 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 @@ -210,8 +213,9 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp switch (method.Name) { case nameof(MemoryExtensions.Contains) - when methodCallExpression.Arguments is [var arg0, var arg1] && - TryUnwrapSpanImplicitCast(arg0, out var unwrappedArg0): + when UseOldBehavior37176 + && methodCallExpression.Arguments is [var arg0, var arg1] + && TryUnwrapSpanImplicitCast(arg0, out var unwrappedArg0): { return Visit( Expression.Call( @@ -219,6 +223,22 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp 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 + && methodCallExpression.Arguments is [var spanArg, var valueArg, ..] + && (methodCallExpression.Arguments.Count is 2 + || methodCallExpression.Arguments.Count is 3 + && methodCallExpression.Arguments[2] is ConstantExpression { Value: null }) + && TryUnwrapSpanImplicitCast(spanArg, out var unwrappedSpanArg): + { + return Visit( + Expression.Call( + EnumerableMethods.Contains.MakeGenericMethod(method.GetGenericArguments()[0]), + unwrappedSpanArg, valueArg)); + } + case nameof(MemoryExtensions.SequenceEqual) when methodCallExpression.Arguments is [var arg0, var arg1] && TryUnwrapSpanImplicitCast(arg0, out var unwrappedArg0) @@ -231,20 +251,37 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp 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.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs index 865f5dc026e..521a504a2c7 100644 --- a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs @@ -371,6 +371,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 222b2e9f8c4..e9d34754632 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs @@ -462,6 +462,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/PrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs index 837646b257c..6d604c275e5 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs @@ -635,6 +635,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 9faa7597b62..a9c9d12e50f 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs @@ -625,6 +625,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);