diff --git a/eng/Versions.props b/eng/Versions.props
index bba3a3f37d..df01f21c29 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -12,7 +12,7 @@
-->
false
release
- net9.0
+ net10.0
16.0.461
diff --git a/src/EntityFramework/Core/Objects/ELinq/LinqExpressionNormalizer.cs b/src/EntityFramework/Core/Objects/ELinq/LinqExpressionNormalizer.cs
index 680c2499fc..3e08a4d8b8 100644
--- a/src/EntityFramework/Core/Objects/ELinq/LinqExpressionNormalizer.cs
+++ b/src/EntityFramework/Core/Objects/ELinq/LinqExpressionNormalizer.cs
@@ -7,6 +7,7 @@ namespace System.Data.Entity.Core.Objects.ELinq
using System.Data.Entity.Utilities;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
+ using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
@@ -38,6 +39,20 @@ internal class LinqExpressionNormalizer : EntityExpressionVisitor
//
private readonly Dictionary _patterns = new Dictionary();
+#if NETSTANDARD2_1
+ // Cache for Enumerable.Contains method lookup to avoid repeated reflection
+ private static readonly Lazy _enumerableContainsMethod = new Lazy(() =>
+ typeof(System.Linq.Enumerable).GetMethods()
+ .Where(method => method.Name == nameof(Enumerable.Contains) && method.GetParameters().Length == 2)
+ .Single());
+
+ // Cache for Enumerable.SequenceEqual method lookup to avoid repeated reflection
+ private static readonly Lazy _enumerableSequenceEqualMethod = new Lazy(() =>
+ typeof(System.Linq.Enumerable).GetMethods()
+ .Where(method => method.Name == nameof(Enumerable.SequenceEqual) && method.GetParameters().Length == 2)
+ .Single());
+#endif
+
//
// Handle binary patterns:
// - VB 'Is' operator
@@ -124,6 +139,32 @@ private static bool IsConstantZero(Expression expression)
((ConstantExpression)expression).Value.Equals(0);
}
+#if NETSTANDARD2_1
+ //
+ // Attempts to unwrap an implicit Span or ReadOnlySpan cast expression to get the underlying array or collection.
+ // This is used to rewrite MemoryExtensions methods that operate on Span/ReadOnlySpan back to Enumerable methods.
+ //
+ private static bool TryUnwrapSpanImplicitCast(Expression expression, out Expression result)
+ {
+ if (expression is MethodCallExpression methodCallExpr
+ && methodCallExpr.Method.Name == "op_Implicit"
+ && methodCallExpr.Method.DeclaringType != null
+ && methodCallExpr.Method.DeclaringType.IsGenericType
+ && methodCallExpr.Arguments.Count == 1)
+ {
+ var genericTypeDefinition = methodCallExpr.Method.DeclaringType.GetGenericTypeDefinition();
+ if (genericTypeDefinition == typeof(Span<>) || genericTypeDefinition == typeof(ReadOnlySpan<>))
+ {
+ result = methodCallExpr.Arguments[0];
+ return true;
+ }
+ }
+
+ result = null;
+ return false;
+ }
+#endif
+
//
// Handles MethodCall patterns:
// - Operator overloads
@@ -132,6 +173,45 @@ private static bool IsConstantZero(Expression expression)
[SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
internal override Expression VisitMethodCall(MethodCallExpression m)
{
+#if NETSTANDARD2_1
+ // .NET 10 made changes to overload resolution to prefer Span-based overloads when those exist ("first-class spans").
+ // Unfortunately, EF6's query translator does not recognize MemoryExtensions methods, so we rewrite e.g.
+ // MemoryExtensions.Contains to Enumerable.Contains here.
+ // See:
+ // https://github.com/dotnet/ef6/issues/2322
+ // https://github.com/dotnet/runtime/issues/109757
+ // https://github.com/dotnet/efcore/pull/35339
+ // Note: This check must occur before base.VisitMethodCall() to detect op_Implicit before it's normalized to Convert.
+ if (m.Method.DeclaringType == typeof(MemoryExtensions))
+ {
+ switch (m.Method.Name)
+ {
+ // Note that MemoryExtensions.Contains has an optional 3rd ComparisonType parameter; we only match when
+ // it's null.
+ case nameof(MemoryExtensions.Contains)
+ when m.Arguments.Count >= 2
+ && (m.Arguments.Count == 2
+ || m.Arguments.Count == 3 && m.Arguments[2] is ConstantExpression constantExpr && constantExpr.Value == null)
+ && TryUnwrapSpanImplicitCast(m.Arguments[0], out var unwrappedSpanArg):
+ {
+ var containsMethod = _enumerableContainsMethod.Value.MakeGenericMethod(m.Method.GetGenericArguments()[0]);
+
+ return Visit(Expression.Call(containsMethod, unwrappedSpanArg, m.Arguments[1]));
+ }
+
+ case nameof(MemoryExtensions.SequenceEqual)
+ when m.Arguments.Count == 2
+ && TryUnwrapSpanImplicitCast(m.Arguments[0], out var unwrappedSpanArg1)
+ && TryUnwrapSpanImplicitCast(m.Arguments[1], out var unwrappedSpanArg2):
+ {
+ var sequenceEqualMethod = _enumerableSequenceEqualMethod.Value.MakeGenericMethod(m.Method.GetGenericArguments()[0]);
+
+ return Visit(Expression.Call(sequenceEqualMethod, unwrappedSpanArg1, unwrappedSpanArg2));
+ }
+ }
+ }
+#endif
+
m = (MethodCallExpression)base.VisitMethodCall(m);
if (m.Method.IsStatic)
diff --git a/test/FunctionalTests/Query/LinqToEntities/ContainsTests.cs b/test/FunctionalTests/Query/LinqToEntities/ContainsTests.cs
index fe948ea948..d59d078095 100644
--- a/test/FunctionalTests/Query/LinqToEntities/ContainsTests.cs
+++ b/test/FunctionalTests/Query/LinqToEntities/ContainsTests.cs
@@ -288,8 +288,8 @@ public void Contains_throws_for_constant_null_list()
public void Contains_on_non_static_collection_of_enums()
{
const string expectedSql =
- @"SELECT
-CASE WHEN ( EXISTS (SELECT
+ @"SELECT
+CASE WHEN ( EXISTS (SELECT
1 AS [C1]
FROM [dbo].[Books] AS [Extent2]
WHERE 0 = [Extent2].[Genre]
@@ -305,5 +305,32 @@ FROM [dbo].[Books] AS [Extent2]
QueryTestHelpers.VerifyDbQuery(query, expectedSql);
}
}
+
+ [Fact]
+ public void EnumerableContains_with_string_array_on_net10_is_translated_to_expected_sql()
+ {
+ // On .NET 10+, the C# compiler prefers Span-based overloads ("first-class spans").
+ // This means array.Contains(value) compiles to MemoryExtensions.Contains(array.AsSpan(), value)
+ // instead of Enumerable.Contains(array, value). EF6 needs to rewrite these back to
+ // Enumerable.Contains for the LINQ interpreter to work.
+ const string expectedSql =
+ @"SELECT
+[Extent1].[Id] AS [Id]
+FROM [dbo].[Books] AS [Extent1]
+WHERE [Extent1].[Title] IN (N'Title1', N'Title2')";
+
+ var array = new[] { "Title1", "Title2" };
+
+ using (var context = new UnicodeContext())
+ {
+ context.Configuration.UseDatabaseNullSemantics = true;
+
+ var query = from book in context.Books
+ where array.Contains(book.Title)
+ select book.Id;
+
+ QueryTestHelpers.VerifyDbQuery(query, expectedSql);
+ }
+ }
}
}
diff --git a/test/UnitTests/Core/Objects/ELinq/LinqExpressionNormalizerTests.cs b/test/UnitTests/Core/Objects/ELinq/LinqExpressionNormalizerTests.cs
index a93553bb31..26436665c7 100644
--- a/test/UnitTests/Core/Objects/ELinq/LinqExpressionNormalizerTests.cs
+++ b/test/UnitTests/Core/Objects/ELinq/LinqExpressionNormalizerTests.cs
@@ -1,8 +1,13 @@
-// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
namespace System.Data.Entity.Core.Objects.ELinq
{
using Xunit;
+#if NET5_0_OR_GREATER
+ using System.Linq;
+ using System.Linq.Expressions;
+ using System.Reflection;
+#endif
public class LinqExpressionNormalizerTests
{
@@ -11,5 +16,177 @@ public void MethodInfo_fields_are_initialized()
{
Assert.NotNull(LinqExpressionNormalizer.RelationalOperatorPlaceholderMethod);
}
+
+#if NET5_0_OR_GREATER
+
+ [Fact]
+ public void MemoryExtensions_Contains_with_ReadOnlySpan_is_rewritten_to_Enumerable_Contains()
+ {
+ // Test: MemoryExtensions.Contains(ReadOnlySpan, T)
+ var array = new[] { "Title1", "Title2", "Title3" };
+ var testValue = "Title1";
+
+ var arrayExpr = Expression.Constant(array);
+ var testValueExpr = Expression.Constant(testValue);
+
+ // Get ReadOnlySpan.op_Implicit method
+ var spanType = typeof(ReadOnlySpan<>).MakeGenericType(typeof(string));
+ var implicitMethod = spanType.GetMethod("op_Implicit", new[] { typeof(string[]) });
+ var spanExpr = Expression.Call(implicitMethod, arrayExpr);
+
+ // Get MemoryExtensions.Contains(ReadOnlySpan, T)
+ // Note: There are both Span and ReadOnlySpan versions, we want ReadOnlySpan
+ var memExtContainsGeneric = typeof(MemoryExtensions).GetMethods(BindingFlags.Public | BindingFlags.Static)
+ .Where(m => m.Name == nameof(MemoryExtensions.Contains)
+ && m.IsGenericMethodDefinition
+ && m.GetParameters().Length == 2
+ && m.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof(ReadOnlySpan<>))
+ .Single();
+ var memExtContains = memExtContainsGeneric.MakeGenericMethod(typeof(string));
+
+ var methodCall = Expression.Call(memExtContains, spanExpr, testValueExpr);
+
+ // Normalize the expression
+ var normalizer = new LinqExpressionNormalizer();
+ var normalized = normalizer.Visit(methodCall);
+
+ // Verify it was rewritten to Enumerable.Contains
+ var normalizedCall = Assert.IsAssignableFrom(normalized);
+ Assert.Equal(nameof(Enumerable.Contains), normalizedCall.Method.Name);
+ Assert.Equal(typeof(Enumerable), normalizedCall.Method.DeclaringType);
+ Assert.Equal(2, normalizedCall.Arguments.Count);
+
+ // The first argument should be the unwrapped array
+ Assert.IsAssignableFrom(normalizedCall.Arguments[0]);
+ var unwrappedArray = ((ConstantExpression)normalizedCall.Arguments[0]).Value;
+ Assert.Same(array, unwrappedArray);
+ }
+
+ [Fact]
+ public void MemoryExtensions_Contains_with_Span_is_rewritten_to_Enumerable_Contains()
+ {
+ // Test: MemoryExtensions.Contains(Span, T)
+ var array = new[] { "Title1", "Title2", "Title3" };
+ var testValue = "Title1";
+
+ var arrayExpr = Expression.Constant(array);
+ var testValueExpr = Expression.Constant(testValue);
+
+ // Get Span.op_Implicit method
+ var spanType = typeof(Span<>).MakeGenericType(typeof(string));
+ var implicitMethod = spanType.GetMethod("op_Implicit", new[] { typeof(string[]) });
+ var spanExpr = Expression.Call(implicitMethod, arrayExpr);
+
+ // Get MemoryExtensions.Contains(Span, T)
+ var memExtContainsGeneric = typeof(MemoryExtensions).GetMethods(BindingFlags.Public | BindingFlags.Static)
+ .Where(m => m.Name == nameof(MemoryExtensions.Contains)
+ && m.IsGenericMethodDefinition
+ && m.GetParameters().Length == 2
+ && m.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof(Span<>))
+ .Single();
+ var memExtContains = memExtContainsGeneric.MakeGenericMethod(typeof(string));
+
+ var methodCall = Expression.Call(memExtContains, spanExpr, testValueExpr);
+
+ // Normalize the expression
+ var normalizer = new LinqExpressionNormalizer();
+ var normalized = normalizer.Visit(methodCall);
+
+ // Verify it was rewritten to Enumerable.Contains
+ var normalizedCall = Assert.IsAssignableFrom(normalized);
+ Assert.Equal(nameof(Enumerable.Contains), normalizedCall.Method.Name);
+ Assert.Equal(typeof(Enumerable), normalizedCall.Method.DeclaringType);
+ Assert.Equal(2, normalizedCall.Arguments.Count);
+
+ // The first argument should be the unwrapped array
+ Assert.IsAssignableFrom(normalizedCall.Arguments[0]);
+ var unwrappedArray = ((ConstantExpression)normalizedCall.Arguments[0]).Value;
+ Assert.Same(array, unwrappedArray);
+ }
+
+#if NET10_0_OR_GREATER
+ [Fact]
+ public void MemoryExtensions_Contains_with_null_comparer_is_rewritten()
+ {
+ // Test: MemoryExtensions.Contains(ReadOnlySpan, T, IEqualityComparer) with null comparer
+ // Note: This 3-parameter overload was added in .NET 10
+ // The fix rewrites this when the comparer parameter is null
+ var array = new[] { "Title1", "Title2", "Title3" };
+ var testValue = "Title1";
+
+ var arrayExpr = Expression.Constant(array);
+ var testValueExpr = Expression.Constant(testValue);
+ var nullComparerExpr = Expression.Constant(null, typeof(System.Collections.Generic.IEqualityComparer));
+
+ // Get ReadOnlySpan.op_Implicit method
+ var spanType = typeof(ReadOnlySpan<>).MakeGenericType(typeof(string));
+ var implicitMethod = spanType.GetMethod("op_Implicit", new[] { typeof(string[]) });
+ var spanExpr = Expression.Call(implicitMethod, arrayExpr);
+
+ // Get MemoryExtensions.Contains(ReadOnlySpan, T, IEqualityComparer)
+ var memExtContainsGeneric = typeof(MemoryExtensions).GetMethods(BindingFlags.Public | BindingFlags.Static)
+ .Where(m => m.Name == nameof(MemoryExtensions.Contains)
+ && m.IsGenericMethodDefinition
+ && m.GetParameters().Length == 3
+ && m.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof(ReadOnlySpan<>)
+ && m.GetParameters()[2].ParameterType.GetGenericTypeDefinition() == typeof(System.Collections.Generic.IEqualityComparer<>))
+ .Single();
+ var memExtContains = memExtContainsGeneric.MakeGenericMethod(typeof(string));
+
+ var methodCall = Expression.Call(memExtContains, spanExpr, testValueExpr, nullComparerExpr);
+
+ // Normalize the expression
+ var normalizer = new LinqExpressionNormalizer();
+ var normalized = normalizer.Visit(methodCall);
+
+ // Verify it was rewritten to Enumerable.Contains
+ var normalizedCall = Assert.IsAssignableFrom(normalized);
+ Assert.Equal(nameof(Enumerable.Contains), normalizedCall.Method.Name);
+ Assert.Equal(typeof(Enumerable), normalizedCall.Method.DeclaringType);
+ Assert.Equal(2, normalizedCall.Arguments.Count);
+ }
+#endif
+
+ [Fact]
+ public void MemoryExtensions_SequenceEqual_is_rewritten_to_Enumerable_SequenceEqual()
+ {
+ // Test that SequenceEqual is also rewritten correctly
+
+ var array1 = new[] { 1, 2, 3 };
+ var array2 = new[] { 1, 2, 3 };
+
+ var array1Expr = Expression.Constant(array1);
+ var array2Expr = Expression.Constant(array2);
+
+ // Get the Span op_Implicit method
+ var spanType = typeof(ReadOnlySpan<>).MakeGenericType(typeof(int));
+ var implicitMethod = spanType.GetMethod("op_Implicit", new[] { typeof(int[]) });
+ var span1Expr = Expression.Call(implicitMethod, array1Expr);
+ var span2Expr = Expression.Call(implicitMethod, array2Expr);
+
+ // Get MemoryExtensions.SequenceEqual(ReadOnlySpan, ReadOnlySpan) - the generic method with 2 parameters
+ // Note: There are both Span and ReadOnlySpan versions, we want ReadOnlySpan
+ var memExtSequenceEqualGeneric = typeof(MemoryExtensions).GetMethods(BindingFlags.Public | BindingFlags.Static)
+ .Where(m => m.Name == nameof(MemoryExtensions.SequenceEqual)
+ && m.IsGenericMethodDefinition
+ && m.GetParameters().Length == 2
+ && m.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof(ReadOnlySpan<>))
+ .Single();
+ var memExtSequenceEqual = memExtSequenceEqualGeneric.MakeGenericMethod(typeof(int));
+
+ var methodCall = Expression.Call(memExtSequenceEqual, span1Expr, span2Expr);
+
+ // Normalize the expression
+ var normalizer = new LinqExpressionNormalizer();
+ var normalized = normalizer.Visit(methodCall);
+
+ // Verify it was rewritten to Enumerable.SequenceEqual
+ var normalizedCall = Assert.IsAssignableFrom(normalized);
+ Assert.Equal(nameof(Enumerable.SequenceEqual), normalizedCall.Method.Name);
+ Assert.Equal(typeof(Enumerable), normalizedCall.Method.DeclaringType);
+ Assert.Equal(2, normalizedCall.Arguments.Count);
+ }
+
+#endif
}
}
diff --git a/test/UnitTests/UnitTests.csproj b/test/UnitTests/UnitTests.csproj
index 0ae59776ae..7cfba91091 100644
--- a/test/UnitTests/UnitTests.csproj
+++ b/test/UnitTests/UnitTests.csproj
@@ -1,12 +1,12 @@
-
+
System.Data.Entity
EntityFramework.UnitTests
- $(NetFrameworkToolCurrent)
+ $(NetFrameworkToolCurrent);$(DefaultNetCoreTargetFramework)
-
+
@@ -16,6 +16,17 @@
+
+
+
+
+
+