Linq: Fixes .NET 10 MemoryExtensions.Contains breaking change in LINQ queries#5819
Open
Linq: Fixes .NET 10 MemoryExtensions.Contains breaking change in LINQ queries#5819
Conversation
In .NET 10, array.Contains() may resolve to MemoryExtensions.Contains(ReadOnlySpan<T>, T) instead of Enumerable.Contains. Since ReadOnlySpan doesn't implement IEnumerable, the existing IsEnumerable() check fails. Changes: - Add IsMemoryExtensionsMethod() helper to detect MemoryExtensions methods - Route MemoryExtensions.Contains to ArrayBuiltinFunctions for proper SQL translation - Add unit tests for MemoryExtensions detection Fixes #5518
- TestMemoryExtensionsContainsExpressionDetection: Verifies MemoryExtensions.Contains<T>(ReadOnlySpan<T>, T) is properly detected - TestBuiltinFunctionVisitorMemoryExtensionsCheck: Simulates .NET 10 expression and validates our fix - TestEnumerableContainsNotDetectedAsMemoryExtensions: Ensures Enumerable.Contains is unaffected These tests programmatically create the expression trees that .NET 10 generates for array.Contains(), verifying the fix handles them correctly.
…uction Addresses review comment: replaced superficial type-name checks with a test that constructs an actual MemoryExtensions.Contains MethodCallExpression and verifies the DeclaringType matches. Removed redundant tests, kept array IsEnumerable and Enumerable negative check. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…moryExtensions.Contains In .NET 10+, array.Contains(x) resolves to MemoryExtensions.Contains(ReadOnlySpan<T>, T) instead of Enumerable.Contains. The expression tree includes an op_Implicit conversion from T[] to ReadOnlySpan<T>. This caused two issues: 1. ConstantEvaluator.PartialEval tried to evaluate op_Implicit(array) as a constant, but ReadOnlySpan<T> is a ref struct that cannot be boxed, causing NotSupportedException. 2. ArrayContainsVisitor.VisitImplicit couldn't find the underlying ConstantExpression (the array) because it was wrapped in the op_Implicit call. Fix: - ConstantEvaluator: Exclude MemoryExtensions methods and op_Implicit on Span/ReadOnlySpan types from constant evaluation (similar to how Enumerable/Queryable are excluded). - ArrayBuiltinFunctions: Add UnwrapSpanImplicitConversion to unwrap op_Implicit and expose the underlying array constant for IN clause generation. This builds on top of the IsMemoryExtensionsMethod detection in BuiltinFunctionVisitor. Fixes #5518 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
adityasa
commented
Apr 29, 2026
adityasa
commented
Apr 29, 2026
adityasa
commented
Apr 29, 2026
adityasa
commented
Apr 29, 2026
- ConstantEvaluator: Replace string comparison with typeof(MemoryExtensions), scope exclusion to Contains method only - BuiltinFunctionVisitor: Replace FullName string check with typeof(MemoryExtensions), scope IsMemoryExtensionsMethod to Contains only - op_Implicit check remains type-scoped (Span/ReadOnlySpan) since ref structs are inherently unevaluable regardless of calling context Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
Test coverage validates each condition in the fix: ConstantEvaluator layer: - MemoryExtensions.Contains is not evaluated (preserved for SQL translation) - op_Implicit on ReadOnlySpan<T> is not evaluated (ref struct cannot be boxed) - op_Implicit on Span<T> is not evaluated - Enumerable.Contains still excluded (regression) - Non-Span op_Implicit (e.g., numeric) is still evaluated (no over-blocking) BuiltinFunctionVisitor layer (supported): - MemoryExtensions.Contains with string[] SQL IN clause - MemoryExtensions.Contains with int[] SQL IN clause - MemoryExtensions.Contains with Guid[] SQL IN clause - Enumerable.Contains still works (regression) BuiltinFunctionVisitor layer (unsupported error): - MemoryExtensions.IndexOf throws DocumentQueryException - MemoryExtensions.SequenceEqual throws DocumentQueryException Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
adityasa
commented
Apr 29, 2026
…runtimes) The previous test used double.op_Implicit(int) which isn't exposed via reflection on .NET 6. Switched to Decimal.op_Implicit(int) which is reliably available. All 12 tests now pass with 0 skipped. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
The explicit check for MemoryExtensions.Contains in CanBeEvaluated is redundant because the Nominator processes bottom-up. The child op_Implicit node on ReadOnlySpan<T>/Span<T> is already blocked, which propagates up to prevent the parent Contains call from being nominated for evaluation. Tests confirm all 12 pass without this check. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
Contributor
|
@sdkReviewAgent-2 |
This was referenced Apr 29, 2026
TRStarr
approved these changes
Apr 30, 2026
xinlian12
reviewed
Apr 30, 2026
Member
|
✅ Review complete (38:00) Posted 2 inline comment(s). Steps: ✓ context, correctness, cross-sdk, design, history, past-prs, synthesis, test-coverage |
kirankumarkolli
previously approved these changes
May 1, 2026
neildsh
previously approved these changes
May 1, 2026
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
Use BuildNet10ContainsExpression<T>() consistently across all translation tests instead of manual reflection, addressing reviewer feedback for code consistency. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
neildsh
approved these changes
May 1, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #5518 - .NET 10 (C# 14) changed
array.Contains(x)to useMemoryExtensions.ContainswithReadOnlySpan<T>instead ofEnumerable.Contains. This breaks the LINQ-to-SQL translator.Architecture / Code Flow
flowchart TD A["<b>User LINQ Query</b><br/>.Where(x => array.Contains(x))"] --> B["<b>C# Compiler</b>"] B -->|".NET 6-9"| C["Enumerable.Contains<T>(array, x)"] B -->|".NET 10+"| D["MemoryExtensions.Contains<T>(<br/>ReadOnlySpan<T>.op_Implicit(array), x)"] C --> E["<b>SQLTranslator.TranslateExpression()</b>"] D --> E E --> F["<b>Step 1: ConstantEvaluator.PartialEval</b><br/>Collapse evaluable sub-expressions to constants"] F --> G["<b>Step 2: ExpressionToSql</b><br/>Translate expression tree to SQL AST"] subgraph step1["Step 1: ConstantEvaluator (Nominator - bottom-up)"] direction TB F1["Visit op_Implicit(array)<br/><i>child node, visited first</i>"] F2{"DeclaringType is<br/>ReadOnlySpan or Span?"} F3["Block: return false<br/><i>ref struct cannot be boxed</i>"] F4["Propagates up:<br/>parent Contains also<br/>not nominated"] F1 --> F2 F2 -->|"Yes"| F3 F3 --> F4 end F --> step1 step1 --> G subgraph step2["Step 2: BuiltinFunctionVisitor (routing)"] direction TB G1{"IsMemoryExtensionsMethod?<br/>DeclaringType == typeof MemoryExtensions<br/>AND Name == Contains"} G2["Route to<br/>ArrayBuiltinFunctions"] G3["Not matched<br/>(IndexOf, SequenceEqual, etc.)"] G4["Throws DocumentQueryException<br/><i>Method X not supported</i>"] G1 -->|"Yes"| G2 G1 -->|"No"| G3 G3 --> G4 end G --> step2 subgraph step3["Step 3: ArrayBuiltinFunctions.VisitImplicit"] direction TB H1["UnwrapSpanImplicitConversion<br/>Detects op_Implicit on Span types"] H2["Extracts original array constant<br/>from args[0].Arguments[0]"] H3["Translates to SQL IN clause"] H1 --> H2 H2 --> H3 end G2 --> step3 step3 --> I["<b>Output SQL</b><br/>SELECT * FROM c WHERE c.Name IN ('a', 'b', 'c')"]Without this fix (on .NET 10)
The
ConstantEvaluatorattempts to evaluateop_Implicit(array)which invokesReadOnlySpan<T>.op_Implicit(T[])and tries to box the ref struct result intoExpression.Constant. This throwsNotSupportedExceptionwhich surfaces as a "Method not supported" error to the user.End-to-End Verification (.NET 10)
Verified with a standalone .NET 10 (
net10.0,LangVersion=preview) console app that references the SDK viaProjectReference. This app could not be checked in because CI pipelines do not yet support .NET 9/10, but the verification was run locally against the Cosmos DB Emulator.All 8 scenarios pass:
string[].Contains(x.Field)root["Name"] IN ("Alice", "Bob")int[].Contains(x.Field)root["Score"] IN (42, 99)Guid[].Contains(x.Field)root["Tag"] IN ("aaaa...")x.Children.Contains("value")(array field on doc)ARRAY_CONTAINS(root["Children"], "child1")x.Children.Containsround-triparray.Contains(x)on simple typeroot IN ("a")enumerable.Contains(x)still worksroot IN ("a")Verification app code (click to expand)
Changes
op_ImplicitonReadOnlySpan<>/Span<>types from partial evaluation (ref structs cannot be boxed)UnwrapSpanImplicitConversion()unwrapsop_Implicitto extract the underlying array before translating to SQLMemoryExtensions.Containsto the array translator (scoped toContainsonly; other methods produce a clear "method not supported" error)Supersedes
This PR supersedes both #5585 and #5477 with a more targeted fix.
.NET Breaking Change Context
This issue stems from a by-design breaking change in C# 14 / .NET 10 where overload resolution now prefers Span/ReadOnlySpan extension methods ("first-class span" support). The .NET runtime team's mitigation is [OverloadResolutionPriority] on Span overloads so the compiler prefers IEnumerable versions. However, LINQ providers (EF Core, Cosmos DB, etc.) still need to handle the case where the compiler emits the Span-based call.
Related discussions: