Skip to content

Treat an inline array literal as a value in LINQ Contains() - #5691

Closed
lahma wants to merge 1 commit into
JasperFx:masterfrom
lahma:fix/linq-inline-array-contains
Closed

lahma wants to merge 1 commit into
JasperFx:masterfrom
lahma:fix/linq-inline-array-contains

Conversation

@lahma

@lahma lahma commented Oct 17, 2026

Copy link
Copy Markdown
Contributor

What fails

session.Query<Swatch>().Where(x => new[] { "Red", "Blue" }.Contains(x.Name))

throws while the query is parsed:

Marten.Exceptions.BadLinqExpressionException : Unable to find any queryable members in expression new [] {"Red", "Blue"}

The same happens with int, Guid and enum literals (both EnumStorage modes), with the static form Enumerable.Contains(new[] { … }, x.Name), with the negated form, and against a duplicated field. An inline array of captured values, new[] { id1, id2 }.Contains(x.Id), fails with a misleading message: Marten does not (yet) support member Guid.id1.

The same values work in a captured local array, and when passed to IsOneOf(). The AOT publishing guide already lists array literals among the values Marten evaluates in a where clause.

Why

Two Contains parsers see this shape. EnumerableContains handles it under C# 13 (net9.0) and for the static form. MemoryExtensionsContains handles C# 14's span binding (net10.0). Both decide which side is the value with TryToParseConstant, which accepted only a ConstantExpression or a closed MemberExpression. An array literal is a NewArrayInit, so it fell through to MemberFor. That looks for a document member, finds none, or, for captured elements, takes the closure field for one. ReduceToConstant already evaluates NewArrayInit, reflectively for constant and captured elements (#5328), so only the recognition was missing. IsOneOf() works because it calls ReduceToConstant directly.

The change

TryToParseConstant also accepts a NewArrayInit whose elements read no lambda parameter, and reduces it with ReduceToConstant. An array that reads the document, such as new[] { x.A, x.B }.Contains("c"), is not a value and fails as before.

One shape is left out on purpose: an array built from a compiled query's own members, new[] { First, Second }.Contains(x.Name). A compiled query plan binds each parameter to one of the query's members by matching values, and an array of two members matches neither. Reducing the literal would bake the planning-time values into every later execution. I checked: without this guard, a second call with different values returns the first call's rows. So an array that reads a compiled query instance (an ICompiledQueryMarker constant) is not reduced, and that shape keeps failing as before. An array literal of constants inside a compiled query works now.

A collection initializer, new List<T> { … }.Contains(…), is a ListInit rather than a NewArrayInit and is not covered here; it still fails as before.

The other callers of TryToParseConstant (Any(), SimpleExpression, the GroupBy HAVING operand, modulo, string.IsNullOrEmpty, dictionary values) either already reduced this shape through ReduceToConstant or now get a value where they used to throw.

The same value-matching gap already affects other compiled query shapes on master, and this PR does not change them: x.Name.IsOneOf(new[] { First, Second }) and x.Name == First + Second both keep their planning-time values.

Tests

LinqTests/Bugs/Bug_inline_array_literal_as_contains_source.cs:

Test Shape Without the fix
string_array_literal new[] { "Red", "Blue" }.Contains(x.Name) fails
string_array_literal_through_static_enumerable_contains Enumerable.Contains(new[] { … }, x.Name), which pins the Enumerable parser on net10.0 fails
negated_string_array_literal !new[] { … }.Contains(x.Name) fails
int_array_literal new[] { 1, 3 } fails
guid_array_literal new[] { new Guid("…"), new Guid("…") } fails
array_literal_of_captured_values new[] { red, blue }.Contains(x.Id) fails ("support member Guid.red")
enum_array_literal(AsInteger / AsString) new[] { Shade.Light, Shade.Dark } both fail
string_array_literal_against_a_duplicated_field same, against Duplicate(x => x.Name); the SQL reads d.name fails
compiled_query_with_an_array_literal a compiled query with an array of constants, executed twice fails
compiled_query_still_refuses_an_array_literal_of_its_own_members the guard above passes, by design; fails if the guard is removed

The instance form binds to Enumerable.Contains on net9.0 and to MemoryExtensions.Contains on net10.0, so running both target frameworks covers both parsers.

Without the fix, 10 of 11 fail on net9.0 and on net10.0. With the fix, 11 of 11 pass on both. The full LinqTests suite passes on both: 1642/1642 on net9.0 and 1642/1642 on net10.0. CompiledQueryTests passes on both as well.

🤖 Generated with Claude Code

`new[] { "a", "b" }.Contains(x.Name)` failed to parse with "Unable to find
any queryable members in expression new [] {"a", "b"}", and an inline array
of captured values, `new[] { id1, id2 }.Contains(x.Id)`, with "Marten does
not (yet) support member Guid.id1". The same values in a captured local
array, or passed to IsOneOf(), have always worked.

The Contains parsers (EnumerableContains, and MemoryExtensionsContains for
the C# 14 span binding) decide which side is the value through
TryToParseConstant, which accepted only a ConstantExpression or a closed
MemberExpression. An array literal is a NewArrayInit, so it fell through to
member resolution. ReduceToConstant already evaluates NewArrayInit, so
TryToParseConstant now accepts one whose elements read no lambda parameter.

An array assembled from a compiled query's own members is left alone. The
plan binds each parameter to one of the query's members by matching values,
an array of several members matches none of them, and reducing it would
freeze the planning-time values into every later execution. That shape keeps
failing the way it did before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copy link
Copy Markdown
Member

Thanks @lahma — this is superseded by #5712, which carries your commit unchanged (you're its author, and co-author on the follow-up).

#5712 adds three things on top:

  • The compiled-query shape works instead of being refused. new[] { First, Second }.Contains(x.Name) is now bound element by element: each element that matches a member is re-read from that member on every execution, and a literal element keeps its value. That also fixes x.Name.IsOneOf(new[] { First, Second }), which you noted keeps its planning-time values on master. The CompiledQueryReadFinder guard is gone, and the refusal test became twice-run tests (string, int, enum in both storages, IsOneOf, member plus literal).
  • The CoreTests failure here was good news. document_store_diagnostics_criteria_shape_matrix expected Dynamic LINQ's inline in (1, 2, 3) / in ("Ann", "Bob") to be refused. With your fix they translate correctly, so the expectations are now Correct.
  • Compiled queries bind parameters by matching values, not by reading the query object: derived values freeze at plan time, and a literal equal to a member's first value gets rebound #5711 tracks the rest of the value-matching gap you described. That covers derived values that freeze (First + Second, ToUpper()), a literal that collides with a member's first-call value, and new List<T> { … }.

Closing this in favour of #5712.


Generated by Claude Code

jeremydmiller added a commit that referenced this pull request Oct 19, 2026
Treat an inline array literal as a value in LINQ Contains(), including a compiled query's own members (supersedes #5691)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants