Repository navigation
Treat an inline array literal as a value in LINQ Contains(), including a compiled query's own members (supersedes #5691) - #5712
Merged
Conversation
`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>
…cution
Builds on the inline-array Contains() fix. `new[] { First, Second }.Contains(x.Name)` in a
compiled query used to be refused, because the plan binds each parameter to one query
member by matching values and an array assembled from two members matches neither:
reducing it would have frozen the planning-time values into every later execution.
CompiledQueryPlan.MatchParameters now matches such an array element by element. Each
element that equals a scalar member's planning value is re-read from that member on every
execution; an element no member accounts for is a literal and keeps its value. The setter
clones the planning array, so it builds no array type at runtime. That also fixes
`x.Name.IsOneOf(new[] { First, Second })`, which has silently kept its planning-time
values on master.
The compiled-query guard in isArrayLiteralValue is gone, and the test that pinned the
refusal now runs the query twice with different values: for string, int and enum members
(both EnumStorage modes), for IsOneOf, and for a member mixed with a literal. All six fail
without the element binding and pass with it.
CoreTests: the diagnostics criteria shape matrix expected Dynamic LINQ's inline
`Score in (1, 2, 3)` / `CustomerName in ("Ann", "Bob")` to be refused. Both are array
literals to LINQ and now translate correctly, so the expectations are Correct.
Co-authored-by: Marko Lahma <marko.lahma@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017FSnat2TKnusp6MdEZgpHj
This was referenced Oct 20, 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.
Supersedes #5691 by @lahma, whose commit is carried here unchanged (with Marko as its author) and as co-author on the follow-up commit.
What fails on master
This throws
BadLinqExpressionException : Unable to find any queryable members in expression new [] {"Red", "Blue"}. The same happens for int, Guid and enum literals, the staticEnumerable.Contains(new[] { … }, x.Name)form, the negated form, and a duplicated field. An array of captured values,new[] { id1, id2 }, fails withMarten does not (yet) support member Guid.id1. See #5691 for the full diagnosis:TryToParseConstantdid not recognise aNewArrayInit.What this PR does
1. lahma's fix (#5691).
TryToParseConstantaccepts aNewArrayInitwhose elements read no lambda parameter and reduces it withReduceToConstant.2. The compiled-query shape now works instead of being refused. #5691 deliberately left
new[] { First, Second }.Contains(x.Name)failing inside a compiled query. The plan binds each parameter to one query member by matching values, and an array assembled from two members matches neither, so reducing it would freeze the planning-time values.CompiledQueryPlan.MatchParametersnow handles an array parameter that no member matches as a whole by matching it element by element:The setter clones the planning array, so it creates no array type at runtime. This also fixes
x.Name.IsOneOf(new[] { First, Second }), which on master silently keeps its planning-time values. #5691'sCompiledQueryReadFinderguard is removed.3. CoreTests expectations. #5691's CI failed in
document_store_diagnostics_criteria_shape_matrix. That matrix expected Dynamic LINQ's inlineScore in (1, 2, 3)andCustomerName in ("Ann", "Bob")to be refused. Both reach LINQ as array literals and now translate correctly (7 of 7 and 3 of 3 rows against the oracle), so the expectations are nowCorrect. TheDiagnosticsCriteriaPoliciesremark that said inlinein (…)still throws is updated to match.Tests
LinqTests/Bugs/Bug_inline_array_literal_as_contains_source.cshas 16 tests. lahma's 11 non-compiled and constant-compiled cases are unchanged. The test that pinned the refusal is replaced by five tests, six cases in all, each running a compiled query twice with different values. The second run is served from the cached plan, so a frozen plan answers it with the first run's rows.compiled_query_with_an_array_literal_of_its_own_membersnew[] { First, Second }.Contains(x.Name)compiled_query_with_is_one_of_over_an_array_literal_of_its_own_membersx.Name.IsOneOf(new[] { First, Second }), which is also broken on mastercompiled_query_with_a_member_and_a_literal_in_one_arraynew[] { Name, "Green" }; the literal keeps its valuecompiled_query_with_an_array_literal_of_int_membersintmemberscompiled_query_with_an_array_literal_of_enum_members(AsInteger / AsString)With the element binding disabled, all six cases fail; with it, 16 of 16 pass.
Local runs on net10.0, before merging current master in:
net9.0 is covered by CI; on net9.0 the instance
Containsform binds toEnumerable.Containsrather thanMemoryExtensions.Contains.Known gap, tracked in #5711
Binding is still by value, so one shape stays wrong: a literal equal to a member's value on the first call gets bound to that member.
new[] { Name, "BB" }planned withName = "BB"then answersName = "aa"with[aa]. Scalar parameters already have this hazard on master:x.Name == Name || x.Name == "BB"does the same. #5711 records it alongside derived values that freeze at plan time (First + Second,Name.ToUpper()) andnew List<T> { … }, which is still refused. It proposes binding by reading the query object instead of by matching values.Generated by Claude Code