Skip to content

Avoid _ collision in bind-*:after property accessor#83572

Draft
chsienki wants to merge 3 commits intodotnet:mainfrom
chsienki:bind-after-discard-collision
Draft

Avoid _ collision in bind-*:after property accessor#83572
chsienki wants to merge 3 commits intodotnet:mainfrom
chsienki:bind-after-discard-collision

Conversation

@chsienki
Copy link
Copy Markdown
Member

@chsienki chsienki commented May 5, 2026

Issue:

When using a bind-MyValue:after="..." tag helper, the emitted code is transformed into a callback on Component.MyValueChanged. As such there is no expression to emit the #line mapping for MyValue meaning things like FAR, GTD can't work on it.

To make these work, PR dotnet/razor#11214 introduced a _ = nameof(Component.MyValue); expression to code gen that can have a #line mapping applied to it.

When passing child content to a RenderFragment<T> the caller can optionally provide a Context variable name for disambiguation between multiple render fragment contexts (see Templated Components). If a user chooses _ as the context name, the generated lambda will have a parameter named _. C# backwards compatibility rules mean that when a lambda has a single parameter named _, that is in actually an accessible parameter named _ and not a discard. Any discards in that scope are invalid as they refer to the parameter.

Combining these two things: if you set a render fragment context to _ and then use the bind-*:after tag helper inside the child content, we emit an assignment to _, but that _ is the lambda parameter, not a discard, and C# emits an error saying it can't assign a string to whatever the type of T is in the render fragment.

Fix:

Instead of a raw discard, emit var (_, _) = (nameof(...), 0); instead. Positional discards in var (_, _) = deconstruction always discard, even with an in-scope _ parameter, and the compiler will still remove this line altogether as unused code.

This is the only place in the compiler that emits a bare _ as an expression, so we don't need to fix it anywhere else.

Microsoft Reviewers: Open in CodeFlow

chsienki and others added 3 commits May 5, 2026 11:29
Repros DevCom 10961132 / dotnet/razor regression introduced by PR
dotnet/razor#11214 ("[Fuse] bind-Value:attribute support", merged
2024-11-18).

That PR added a design-time `_ = nameof(...)` discard line for
@bind-x:set/event/after directive parameters so the IDE has a target
for FAR/Hover. The discard is emitted unconditionally as the literal
token `_`, which collides with user-named `_` lambda parameters
introduced by `Context="_"` on a templated parent component:

    <OuterComponent Context="_">                  // _ : FilterContext
        <MyComponent @bind-Value="ParentValue"
                     @bind-Value:after="OnAfter" />
    </OuterComponent>

The generated code becomes (paraphrased):

    AddAttribute(1, "ChildContent",
        (RenderFragment<FilterContext>)((_) => (__builder2) => {
            // ... bind:after lowering ...
            _ = nameof(MyComponent.Value);   // assignment to _ : FilterContext
        }));

Roslyn parses `_ = nameof(...)` as an assignment to the in-scope `_`
parameter rather than a discard, producing CS0029 (cannot convert
string to FilterContext) plus cascading CS1662s on the wrapping
template lambdas.

This commit only adds the failing test and baselines. The fix to
WriteDesignTimePropertyAccessor (wrapping the discard in a `{ }` block
scope) follows in a separate commit.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Emit `var (_, _) = (nameof(...), 0);` instead of `_ = nameof(...);` in
WriteDesignTimePropertyAccessor so that `_` is unambiguously a discard
pattern.

A bare `_ = ...` resolves to any outer `_` lambda parameter (introduced
e.g. by `Context="_"` on a templated parent component), turning the
discard into a typed assignment that fails with CS0029 / cascading
CS1662 errors. Wrapping the discard in `{ }` doesn't help because C#
block scope doesn't shadow outer parameters of the same name; using
`var (_, _)` deconstruction does, because positional discards in a
deconstruction are always discards regardless of any in-scope `_`.

Fixes the BindToComponent_WithAfter_InsideTemplateWithDiscardContext
test added in the previous commit.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Mechanical baseline regeneration to reflect the new
`var (_, _) = (nameof(...), 0);` discard pattern emitted by
WriteDesignTimePropertyAccessor in the previous commit.

No source changes; only `.codegen.cs` and `.mappings.txt` files
for the existing BindTo* tests that already covered
@bind-x:set, @bind-x:event, and @bind-x:after lowering.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@Mike-E-angelo
Copy link
Copy Markdown

Thank you so very much for attending to this! 🙏🙏🙏

Copy link
Copy Markdown
Member

@davidwengier davidwengier left a comment

Choose a reason for hiding this comment

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

LGTM.

Unless you have any ideas for allowing the razor generator to be specially blessed so it, and it alone, can use unspeakable identifiers 😁

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.

3 participants