Skip to content

Sanitize expression variable names when generating precompiled query code - #39062

Open
philcarbone wants to merge 1 commit into
dotnet:mainfrom
philcarbone:fix/precompiled-query-variable-names
Open

philcarbone wants to merge 1 commit into
dotnet:mainfrom
philcarbone:fix/precompiled-query-variable-names

Conversation

@philcarbone

@philcarbone philcarbone commented Sep 22, 2026 •

Copy link
Copy Markdown

Fixes #39061

LinqToCSharpSyntaxTranslator turns EF's shaper expression trees into C# for precompiled queries. The names of variables, lambda parameters, catch variables and labels in those trees are arbitrary strings, and the relational shaper derives some of them from user-configured JSON property names, which need not be valid C# identifiers. The translator uniquified those names but emitted them verbatim, so a precompiled query whose shaper contained such a name produced an interceptor that failed to compile with CS1001 (found while reviewing #39014, where a block variable named after a JSON property called 1!NOT VALID COLLECTION; produced invalid code).

Changes

  • One sanitizer for every emitted name. The identifier sanitizer that PrecompiledQueryCodeGenerator already applied to runtime constant names moves into the translator as SanitizeIdentifierName, and the generator calls it there. Invalid characters become _, and so do Unicode formatting characters (category Cf), which are valid inside an identifier but ignored when identifiers are compared, so two names differing only by one would otherwise declare the same identifier twice; a leading digit gets a _ prefix, and a reserved keyword gets one too, so generated identifiers are always plain identifiers rather than verbatim ones (_class). Captured variables, which are emitted as the caller wrote them, are compared against generated names without their formatting characters for the same reason. Contextual keywords are valid identifiers in these positions and are left alone, value among them, since generated property setters use it.
  • Every path that emits a name from the expression tree now goes through it: block variables and lifted temporaries (via UniquifyVariableName), lambda parameters, catch variables and goto labels. Reflection-provided names (types, members, methods, enum members) are already valid identifiers, and captured variables in VisitParameter are still emitted verbatim on purpose, because they have to match the declaration in the enclosing generated code.
  • Lambda parameters keep intentional shadowing but never collide. A parameter whose raw name matches a variable from an enclosing scope is still emitted as shadowing it, as before. It is uniquified when sanitization would make it collide with an enclosing variable whose raw name differs, since that would rebind references to that variable inside the lambda, and whenever it collides with a lifted variable whatever the raw names, since lifted declarations are emitted inside the lambda body, where a parameter of the same name does not compile (CS0136).
  • Catch variables get their own stack frame, registered before the filter and body are translated, so the declaration and the references to it agree on the name. Declaring the same ParameterExpression in an enclosing block and as a catch variable is now rejected the way VisitBlock rejects it, instead of silently producing two unrelated locals.
  • A captured variable that a generated name would shadow is now rejected. Captured variables are emitted verbatim, so if a variable the translator generated is in scope under that name, the reference would silently bind to it. That is now a NotSupportedException (the generator records the query and moves on) rather than wrong code. Names reserved for the caller's own declarations, the constant replacements, are unaffected, since they are exactly what such a reference is meant to resolve to.
  • Generated names and the caller's own names are kept apart. Before translating, the translator collects the names the expression references but does not declare; those belong to the code the result is emitted into and are written out as they are, so nothing generated afterwards takes one of them. Without that, the collision goes wrong in both directions: a generated name chosen later rebinds an earlier reference, and a reference appearing later resolves to the generated variable instead of the caller's. A lifted invocation argument is registered by identity rather than matched by name, so it is not mistaken for one of these.
  • Lifted constant names are sanitized too. PrecompiledQueryCodeGenerator emitted var {name} = ... straight from the lifted constant's parameter name, and that name is whatever the caller passed to ILiftableConstantFactory.CreateLiftableConstant, so a provider can supply one that is not a valid C# identifier. A constant named Class generated var class = .... The parameter is renamed rather than just its declaration, since the same name is what the translated executor emits for every reference to that constant.
  • The generated executor's own identifiers are all reserved. It declares five, and not all of them were in the reserved set, so a lifted constant named queryContext or DbContext collided with the executor's own parameters. The names are constants used both to emit the declarations and to build the reservation set, so the two cannot drift apart, and the set is handed to the translator for the executor and the lifted initializers as names declared in the enclosing scope, so a shaper variable named dbContext comes out as dbContext0 rather than shadowing the parameter, and the executor lambda's own queryContext parameter no longer shadows the method's either.
  • Runtime constant fields get a leading underscore. A runtime constant becomes a private static readonly field of the generated interceptors class, under a name that comes from the model (a JSON property name plus Bytes) or from a provider (RuntimeConstantExpression accepts any name and uppercases its first character). The class's own members (Query{n}_… interceptors, executor fields and executor generators, UnsafeAccessor_… methods, the class itself) and the executor's parameters all start with a letter, and the translated code also refers to types by their simple names (Encoding, Snapshot, …). A provider constant named Query1_Executor produced CS0102, one named EntityFrameworkCoreInterceptors CS0542, and one named Encoding broke every runtime constant initializer, since those are Encoding.UTF8.GetBytes(...) (CS1061). The model alone can reach the first kind, with a JSON property named after the unsafe accessor of a …Bytes property that materialization sets through its private setter. Prefixing the field with _ puts it outside the generated members and the executor's identifiers at once, instead of reserving them one family at a time. The prefixed name is what gets sanitized, since the prefix can complete a reserved token: _arglist is an identifier and __arglist is not. A type can start with an underscore too, and no convention on the field side can rule that out, so the translator now writes any type whose simple name is in use in the scope it emits into, as a constant replacement, a declared field or a variable, fully qualified with global::. A provider constant named Foo whose initializer read a static member of a type _Foo produced CS1061 before. Lifted constants are already uniquified against these fields, and cannot shadow a type or a generated member themselves, since LiftableConstantExpression lowercases their first character.
  • A query that fails to precompile no longer breaks the file. Its region and operator interceptors are written before its executor is generated, so a failure there left interceptors calling an executor that was never emitted, plus a half-written executor method. The generator now generates each query's code into a builder of its own and adds it to the file only once the query has succeeded, records the runtime constants the query declares so that exactly those are taken back when it fails, and gives each query its own runtime constant processor, so a rolled-back query cannot leave a deduplication entry that starves a later query of its field. Constants shared between queries are still emitted as one field: the generator keeps the declared constants by initializer as well as by value, since each shaper evaluates a JSON property name into a byte array of its own, and rolls that map back with the rest. The collected namespaces and unsafe accessors are deliberately left alone: the translator caches every accessor it has generated and re-adds it on each later translation, so removing one could not hold, and an unused accessor with its usings is inert. The test helper also used to stop as soon as a test's error asserter had run, without compiling what was generated, which is why this went unnoticed; it now compiles it.
  • A failed translation no longer leaks state. The generator catches a per-query failure and continues with the same translator instance, so TranslateCore resets the scope stack and the lifted state when a translation throws, and RuntimeModelLinqToCSharpSyntaxTranslator restores its member-access replacements in a finally. Without the latter, a query that failed while replacements were set left them applied to the runtime-constant initializers generated afterwards.

Tests

New LinqToCSharpSyntaxTranslatorTest cases, each of which fails without the corresponding change: an invalid block variable, lambda parameter, catch variable and label name; two variables differing only by a formatting character, and a generated local equal to a captured variable up to one; reserved keyword names, and var, async and value, which must stay as they are; a lambda parameter colliding with a lifted variable under differing and identical raw names, and with an outer variable after sanitization; a lambda parameter, a block variable and an invocation-lifted local whose names would otherwise collide with a captured variable, in both orders, with a captured variable naming a constant replacement as the counter-case; a lifted constant named after a C# keyword, after each of the executor's own identifiers and after another constant's sanitized name; shaper variables named after all five executor identifiers; a runtime constant named after the executor field, after the interceptors class, after a type the generated code uses, after a name that becomes a reserved token once prefixed, after a type with a leading underscore, and after an unsafe accessor, the last through model configuration alone; two JSON property names differing only by a formatting character; translator cases for a type, a generic type and a type shadowed by a local being written out fully qualified; a query that fails to precompile next to one that succeeds, one followed by a query reusing its runtime constant, and two queries sharing a runtime constant that must produce one field, all as precompiled query tests that compile and run the generated code on SQLite and SQL Server; the duplicate catch declaration; and a translation reused after a failure. The existing RuntimeConstantExpression test now expects the field as _NumberBytes; it was the only test that pinned the old name.

A lambda parameter that already had the same name as a variable in scope still shadows it, which Variable_with_same_name_in_lambda_does_not_get_renamed and Same_parameter_instance_is_used_twice_in_nested_lambdas pin down; this PR does not change that.

EFCore.Design.Tests passes locally, as do the SQLite precompiled-query and compiled-model functional classes end to end, the latter with no baseline changes, so generated compiled models are unaffected.

Two things I noticed nearby and did not touch, both pre-existing: in the shapes where a lambda parameter meets a lifted variable, the translator moves the pending lifted declaration (var i = 8;) into the lambda body, so it is evaluated on every invocation rather than once where the expression tree evaluates it; and a catch body that is an expression with lifted statements loses them, since only the expression is wrapped in a block. The new tests assert today's output for the first. Happy to open issues for either if they aren't already known.

No public API change: the new method is on a type in an .Internal namespace and the Design API baseline is unaffected.


        Summary of the changes
        - Detail 1
        - Detail 2

        Fixes #bugnumber
  • Tests for the changes have been added (for bug fixes / features)
  • Code follows the same patterns and style as existing code in this repo

Copilot AI lite review requested due to automatic review settings September 22, 2026 14:58
@philcarbone
philcarbone requested a review from a team as a code owner September 22, 2026 14:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Parameter/catch names and reserved keywords can still produce invalid generated C# code.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 Medium severity

Open (1)
What changed in this PR

Sanitizes expression-tree variable names before generating precompiled C# query code.

Changes:

  • Moves identifier sanitization into LinqToCSharpSyntaxTranslator.
  • Reuses it for runtime constant names.
  • Adds coverage for invalid block-variable names.
File Summary Review comments
test/​EFCore.Design.Tests/​Query/​LinqToCSharpSyntaxTranslatorTest.cs Tests invalid block-variable sanitization. —
src/​EFCore.Design/​Query/​Internal/​PrecompiledQueryCodeGenerator.cs Reuses the shared sanitizer. —
src/​EFCore.Design/​Query/​Internal/​LinqToCSharpSyntaxTranslator.cs Adds sanitization and integrates it with name uniquification. Moderate: lambda and catch parameter names can bypass sanitization (3 votes). Moderate: reserved keywords remain valid identifiers (1 vote).

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/EFCore.Design/Query/Internal/LinqToCSharpSyntaxTranslator.cs
Copilot AI review requested due to automatic review settings September 22, 2026 15:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Reserved C# keywords can still produce invalid generated identifiers.

Review effort: Lite
Findings: 1 Medium severity

Open (1)

@philcarbone
philcarbone force-pushed the fix/precompiled-query-variable-names branch 2 times, most recently from 4459a7c to 443d8e4 Compare September 22, 2026 15:51
Copilot AI review requested due to automatic review settings September 22, 2026 15:58
@philcarbone
philcarbone force-pushed the fix/precompiled-query-variable-names branch from 443d8e4 to edba99f Compare September 22, 2026 15:58
@philcarbone
philcarbone force-pushed the fix/precompiled-query-variable-names branch from edba99f to 5b1fa41 Compare September 22, 2026 16:00
@philcarbone

philcarbone commented Sep 22, 2026 •

Copy link
Copy Markdown
Author

On the "reserved keywords" point from the second review summary: Roslyn would emit a keyword-named variable as a verbatim identifier (var @class = 3;), which compiles, but a contextual keyword such as await would come out unescaped. In 1f3260b the sanitizer prefixes reserved and contextual keywords too, so generated identifiers are always plain (_class, _await). Variable_named_like_a_keyword_is_sanitized covers both.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The changes are covered by regression tests and no blocking issues were identified.

Review effort: Lite
Findings: None

Resolved since last review (1)

Copilot AI review requested due to automatic review settings September 22, 2026 16:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Two unresolved translator issues remain, including a critical name-collision issue.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity

Open (1)

Comment thread src/EFCore.Design/Query/Internal/LinqToCSharpSyntaxTranslator.cs Outdated
Copilot AI review requested due to automatic review settings September 22, 2026 16:51
@philcarbone
philcarbone force-pushed the fix/precompiled-query-variable-names branch from 5b1fa41 to 1f3260b Compare September 22, 2026 16:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Nested-scope sanitized-name collisions remain unresolved and may alter variable binding.

Review effort: Lite
Findings: 1 High severity

Open (1)

Copilot AI review requested due to automatic review settings September 22, 2026 17:03
@philcarbone
philcarbone force-pushed the fix/precompiled-query-variable-names branch from 1f3260b to 3293518 Compare September 22, 2026 17:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Two unresolved moderate issues affect identifier collision safety and translator state cleanup.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 Medium severity

Open (1)
Resolved since last review (1)

Comment thread src/EFCore.Design/Query/Internal/LinqToCSharpSyntaxTranslator.cs Outdated
Copilot AI review requested due to automatic review settings September 22, 2026 17:38
@philcarbone
philcarbone force-pushed the fix/precompiled-query-variable-names branch from 3293518 to 75d2660 Compare September 22, 2026 17:38
@philcarbone

Copy link
Copy Markdown
Author

To close this out systematically rather than one finding at a time, I went through every place the translator turns a runtime string into an identifier. Everything that can carry a name from an expression tree now goes through SanitizeIdentifierName before uniquification: block variables and all lifted temporaries (via UniquifyVariableName), lambda parameters (with the sanitization-only collision handling above), catch variables (own stack frame), and, added in 75d2660, goto labels, which were still emitted raw (goto 1!not valid;; before, goto _1_not_valid_; now, with a test). The remaining raw emissions are reflection-provided names (types, members, methods, enum members), the generator's own constant-replacement names (already sanitized in PrecompiledQueryCodeGenerator), and captured variables in VisitParameter, which are emitted verbatim on purpose because they must match the caller's declaration. Translator + generator classes: 176 pass; SQLite precompiled-query classes end to end: 131 pass, 4 skipped.

@philcarbone
philcarbone force-pushed the fix/precompiled-query-variable-names branch from a3761bb to 447775f Compare September 23, 2026 15:05
Copilot AI review requested due to automatic review settings September 23, 2026 15:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Runtime-constant fields can collide with executor variables, causing incorrect or uncompilable generated queries.

Review effort: Balanced
Findings: 1 High severity

Open (1)
Resolved since last review (1)

@AndriySvyryd AndriySvyryd self-assigned this Sep 23, 2026
Copilot AI review requested due to automatic review settings September 23, 2026 18:45
@philcarbone
philcarbone force-pushed the fix/precompiled-query-variable-names branch from 447775f to 855ee58 Compare September 23, 2026 18:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The test helper can dereference a null result when all located queries fail precompilation.

Review effort: Balanced
Findings: 1 High severity

Open (1)

Copilot AI review requested due to automatic review settings September 23, 2026 18:58
@philcarbone
philcarbone force-pushed the fix/precompiled-query-variable-names branch from 855ee58 to f46f3cf Compare September 23, 2026 18:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Runtime-constant prefixing can still generate a reserved C# identifier and break compilation.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 2 High severity

Open (2)

Comment thread src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs Outdated
Copilot AI review requested due to automatic review settings September 23, 2026 19:14
@philcarbone
philcarbone force-pushed the fix/precompiled-query-variable-names branch from f46f3cf to c34cf3b Compare September 23, 2026 19:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The per-query processor loses expression-based cross-query runtime-constant deduplication, potentially emitting duplicate static fields.

Review effort: Balanced
Findings: 1 High severity

Open (1)
Resolved since last review (1)

Copilot AI review requested due to automatic review settings September 23, 2026 19:32
@philcarbone
philcarbone force-pushed the fix/precompiled-query-variable-names branch from c34cf3b to aca0cf8 Compare September 23, 2026 19:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The review found no unresolved issues, and the changes include comprehensive regression coverage.

Review effort: Balanced
Findings: 1 High severity

Open (1)

@philcarbone

Copy link
Copy Markdown
Author

A note on the churn here, since this PR has taken an unusual number of rounds.

An AI assistant was working this PR, including the replies to the review findings. For several rounds it fixed each finding in isolation rather than the rule underneath them, which is why the same class of problem kept reappearing one case at a time: a lambda parameter, then a lifted temporary, then a captured variable. It also twice claimed the area was fully audited when it was not.

I interjected and redirected it to derive the invariant first. That is what the current shape is: the translator collects the names the expression references but does not declare, before generating anything, and nothing it generates may take one of them; runtime constant fields live in a namespace of their own; a failed query leaves nothing of itself behind. Each finding since was tested before it was answered, including the ones that did not reproduce, and the evidence for those is in the threads rather than another patch.

Apologies for the notification noise.

Copilot AI review requested due to automatic review settings September 24, 2026 16:59
@philcarbone
philcarbone force-pushed the fix/precompiled-query-variable-names branch from aca0cf8 to 59a7d38 Compare September 24, 2026 16:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Runtime-constant fields can still shadow provider type names that begin with an underscore.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 2 High severity

Open (2)

Comment thread src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unicode format characters can still bypass uniqueness checks and produce colliding C# identifiers.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 3 High severity

Open (3)

Comment thread src/EFCore.Design/Query/Internal/LinqToCSharpSyntaxTranslator.cs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Executor-owned names remain unreserved during final translation, and cumulative snapshots introduce quadratic generation overhead.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 3 High severity · 1 Medium severity

Open (4)
Resolved since last review (1)

Comment thread src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs
Comment thread src/EFCore.Design/Query/Internal/PrecompiledQueryCodeGenerator.cs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The broad identifier-binding and rollback changes affect intricate code-generation state and merit final human validation.

Review effort: Balanced
Findings: 2 High severity

Open (2)
Resolved since last review (2)

…code

- Block variable and lambda parameter names in expression trees are arbitrary strings; the shaper derives some of them from user-configured JSON property names, which need not be valid C# identifiers. LinqToCSharpSyntaxTranslator only uniquified those names, so the generated interceptor failed to compile with CS1001 for such a name.
- Move the identifier sanitizer that PrecompiledQueryCodeGenerator already applied to runtime constant names into the translator and apply it to every variable name it emits; the generator now calls the shared method. Names that are reserved keywords are prefixed so that generated identifiers never have to be verbatim; contextual keywords are left as they are. Unicode formatting characters become underscores as well, since C# ignores them when comparing identifiers, so two names differing only by one would otherwise be one identifier declared twice; captured variables are compared against generated names without them for the same reason.
- Lambda parameters that do not clash with a constant replacement, catch-block variables and goto labels were emitted from their raw names as well; they now go through the same sanitizer. A lambda parameter that already had the same name as a variable in scope still shadows it, but a collision that only sanitization creates is uniquified, as is any collision with a lifted variable, since lifted declarations are emitted inside the lambda body where a parameter of the same name does not compile.
- Register the catch variable in its own stack frame, so that its declaration and the references to it agree on the name, and reject the same ParameterExpression being declared by an enclosing block and by a catch clause, as VisitBlock does.
- Collect the names the expression references but does not declare before translating it. Those belong to the caller and are emitted as they are, so nothing generated afterwards may take one of them, in either order: a name chosen later would rebind an earlier reference, and a reference appearing later would resolve to the generated variable. A lifted invocation argument is registered by identity rather than matched by name, so that it is not mistaken for one of them.
- Reset the scope stack and lifted state when a translation throws, and restore the member access replacements of RuntimeModelLinqToCSharpSyntaxTranslator in a finally: the translator is reused for later queries after PrecompiledQueryCodeGenerator records a failure, and neither may leak into them.
- Sanitize the names of lifted constants in PrecompiledQueryCodeGenerator as well. Those come from whoever called ILiftableConstantFactory.CreateLiftableConstant, so a provider can supply a name that is not a valid C# identifier, and the generator emitted it verbatim. The parameter is renamed rather than just its declaration, since the same name is what the translated executor emits for every reference to that constant.
- Reserve every identifier the generated executor declares for itself. They are now constants used both to emit those declarations and to build the reservation set, so a lifted constant named after one of them, such as queryContext or DbContext, no longer collides with the executor's own parameters; the same set is passed to the translator as the names declared in the enclosing scope, so a shaper variable named after one is uniquified instead of shadowing it.
- Prefix the names of runtime constant fields with an underscore. A runtime constant becomes a field of the generated interceptors class under a name the model or a provider chose, and everything else that class refers to by simple name starts with a letter: its own members (interceptors, executor fields and generators, unsafe accessors, the class itself), the types the translated code uses, and the executor's parameters. A provider constant named Query1_Executor, EntityFrameworkCoreInterceptors or Encoding made the file uncompilable, and a JSON property named after the unsafe accessor of a Bytes-suffixed setter did so from the model alone; the prefix keeps such a field out of the generated members and the executor's identifiers at once rather than reserving them one family at a time, and the prefixed name is what gets sanitized, since the prefix can complete a reserved token such as __arglist. A type can start with an underscore as well, so the translator writes any type whose simple name is in use in the scope it emits into, as a constant replacement, a declared name or a variable, fully qualified with global:: rather than by its simple name.
- Generate each query's code into a builder of its own and add it to the file only once the query has succeeded, record the runtime constants a query declares so that exactly those are taken back when it fails to precompile, and give each query its own runtime constant processor so that a rolled-back query cannot leave a deduplication entry behind for a later query that reuses the same constant. Constants shared between queries still share one field: the generator matches them by initializer as well as by value, since each shaper evaluates a JSON property name into a byte array of its own, and rolls that map back too. The region and the operator interceptors are written before the executor is generated, so a failure while generating the executor left interceptors calling an executor that was never emitted, and a half-written executor method, making the whole file uncompilable for the queries that did succeed.
- Add translator unit tests for each of these, and precompiled query tests for a lifted constant named after a C# keyword, after the executor's own identifiers and after another constant's sanitized name, for a runtime constant named after the executor field, the interceptors class, a type the generated code uses, a name that is reserved once prefixed, a type with a leading underscore, and an unsafe accessor, for a query that fails to precompile alongside one that succeeds and for one that fails between two that succeed, for a lifted constant named like a runtime constant field, for one followed by a query reusing its runtime constant, and for two queries sharing a runtime constant. The existing RuntimeConstantExpression test expects the prefixed field name. The precompiled query test helper now compiles what was generated even when a test expects a precompilation error, which is what let the partial output go unnoticed.

Fixes dotnet#39061

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

The broad, stateful code-generation and rollback changes warrant final human validation despite extensive coverage.

Review effort: Balanced
Findings: None

Resolved since last review (2)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Precompiled query generation emits expression variable names verbatim, producing invalid C# for names that are not identifiers

3 participants