Repository navigation
Compiled queries: plan from a generated template, and refuse values computed from the query object (#5711, options 2 and 3) - #5717
Merged
Conversation
…puted from the query object A compiled query's plan binds each SQL parameter to the query member whose value it carries. Planning from the caller's own instance let a literal that happened to equal a member's first value bind to that member, and a value computed from members (First + Second, Name.ToUpper(), a list initializer) matched no member and froze at its planning value. Both returned wrong rows on every later execution, with no error. - Always plan from a generated unique-value template, falling back to the caller's instance only when a template cannot be built. - Refuse at plan time any sub-expression of QueryIs() that reads the query object without being a direct member read (or a value-preserving conversion of one, or an inline array of them). - The DateTime and DateTimeOffset planning values were all the same value, so a query with two date members could not be planned from a template. - A template built from constructor arguments now stores its own values before MatchParameters reads them. Part of #5711 (options 2 and 3); structural binding stays open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017FSnat2TKnusp6MdEZgpHj
A freshly constructed query passed the template's uniqueness check whenever its members differed from one another, so a query with one string member planned with that member null. A null matches no parameter, so the value froze. Before #5711 this path was only reached when the caller's own values were not unique; planning from the template every time exposed it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017FSnat2TKnusp6MdEZgpHj
x.Color == Color compiles to Convert(x.Color, Int32) == Convert(Color, Int32). The parser strips that conversion and binds the enum member through the enum storage, so the refusal must not judge it by its integer value, which under string storage matches nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017FSnat2TKnusp6MdEZgpHj
Merged
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.
Part of #5711. This covers options 2 and 3 only. The structural fix (option 1: bind by reading the query object instead of matching values) stays open on the issue.
Problem
A compiled query's plan binds each SQL parameter to a query member by matching values. A parameter whose value equals a member's planning value is re-read from that member on every execution, and any other parameter is treated as a literal. Two consequences:
x.Name == First + Second,x.Name == Name.ToUpper()ornew List<string> { Name }.Contains(x.Name)produce a value that equals no single member. The plan therefore treats it as a literal, and every later execution silently reuses the value the plan was built with.The change
Option 2: always plan from a generated template (
CompiledQueryPlan.CreateQueryTemplate/TryCreateUniqueTemplate).IQueryPlanningtype still supplies its own values.InvalidCompiledQueryException.nullor its default could pass the uniqueness check while that member matched nothing.DateTimeandDateTimeOffsetfinders now produce distinct values for each member. Before, they handed out the same value every time.Option 3: refuse a value computed from the query object (new
QueryObjectReads, called fromQueryCompiler.BuildQueryPlan).Every largest sub-expression of
QueryIs()that reads the query object, and uses no lambda parameter, is evaluated on the client. Each one has to bind to a member, or planning refuses it, naming the expression. These shapes are allowed:inttoint?;NewArrayInitwhose elements each bind, which Treat an inline array literal as a value in LINQ Contains(), including a compiled query's own members (supersedes #5691) #5712 binds element by element. That includes theop_ImplicittoReadOnlySpan<T>wrapper C# 14 inserts forMemoryExtensions.Contains.The message:
Behaviour change
A compiled query that used to run with a frozen value now throws when it is planned. That is intended: it was returning wrong results silently.
docs/documents/querying/compiled-queries.mddescribes the limitation and the workaround (a computed property on the query type, as the existingSkipCountsample already does).Binary compatibility
No public signature changes.
QueryObjectReadsis internal, and the template changes are inside existing private and internal members.Tests
LinqTests/Bugs/Bug_5711_compiled_query_parameter_binding.cs: 10 tests, each running the query twice with different values where it matters.ToUpper(),List<T>initializer, a derived local captured in a closureint→int?,(int)enum, a member read through a closure, twoDateTimemembersThings the full runs caught along the way, all now covered:
EnumStorage.AsString;Local, net10.0, Release, against PostgreSQL:
markdownlint and cspell are clean on the docs page.
Generated by Claude Code