Skip to content

Conversation

Copilot
Copy link
Contributor

@Copilot Copilot AI commented Sep 25, 2025

Fixes #53269

Problem

Local functions defined within @{ ... } blocks in Razor components can capture RenderTreeBuilder instances from their parent scope, leading to incorrect rendering behavior. This pattern appears to work but actually corrupts the rendering output instead of properly writing to child component render fragments.

@{
    void RenderTree(int depth, int maxDepth)
    {
        if (depth >= maxDepth) return;
        
        <FluentTreeItem Text="item">
            @{ RenderTree(depth + 1, maxDepth); }  // ❌ Uses wrong builder
        </FluentTreeItem>
    }
}

The issue occurs because C# scoping rules cause the local function to capture the RenderTreeBuilder from the parent context rather than using the builder that should be passed to the RenderFragment.

Solution

This PR adds a new analyzer diagnostic ASP0029 that detects local functions which access RenderTreeBuilder methods from captured variables in their parent scope.

New Diagnostic: ASP0029

  • Severity: Error
  • Category: Usage
  • Message: "Local function '{functionName}' accesses RenderTreeBuilder from parent scope, which can cause incorrect rendering behavior. Consider making it a static method or regular instance method that takes RenderTreeBuilder as a parameter."

Detection Logic

The analyzer intelligently identifies problematic patterns while allowing safe alternatives:

Allowed (Safe Patterns):

  • Static local functions (cannot capture from parent scope)
  • Local functions that take RenderTreeBuilder as a parameter
  • Local functions that don't use RenderTreeBuilder at all

Detected (Problematic Patterns):

  • Local functions accessing RenderTreeBuilder from captured variables
  • Nested local functions with the same issue

Example

Before (causes runtime issues):

var builder = new RenderTreeBuilder();

void LocalFunction()  // ❌ ASP0029: Captures builder from parent scope
{
    builder.OpenElement(0, "div");
    builder.CloseElement();
}

After (recommended approaches):

// Option 1: Static local function with parameter
static void LocalFunction(RenderTreeBuilder builder)  // ✅ Safe
{
    builder.OpenElement(0, "div");
    builder.CloseElement();
}

// Option 2: Regular method
public RenderFragment CreateFragment() => builder =>  // ✅ Safe
{
    builder.OpenElement(0, "div");
    builder.CloseElement();
};

Testing

Added comprehensive test coverage with 7 test cases covering:

  • Local functions with captured RenderTreeBuilder (detected)
  • Static local functions (allowed)
  • Local functions with RenderTreeBuilder parameters (allowed)
  • Nested local function scenarios
  • Local functions without RenderTreeBuilder usage (allowed)

All existing RenderTreeBuilder analyzer tests continue to pass.

Impact

This change helps developers avoid a subtle but problematic pattern that can cause rendering corruption in Blazor applications. The analyzer provides clear, actionable feedback at compile time rather than allowing runtime failures.

Original prompt

This section details on the original issue you should resolve

<issue_title>Prevent use of local functions inside markup</issue_title>
<issue_description>[Edit by @SteveSandersonMS] This issue was originally reported by @verdie-g as follows below the line. On investigation the problem is that C# has added a new syntax that doesn't work in Razor.

The Razor compiler allows arbitrary C# code within @{ ... } blocks. Unfortunately this means it allows the use of local functions in a way that confuses the parsing logic, causing it to use the wrong __builder instance. Example:

<FluentTreeView>
@{
    RenderTree(0, 3);

    void RenderTree(int depth, int maxDepth)
    {
        if (depth >= maxDepth)
        {
            return;
        }

        <FluentTreeItem Text="item">
            @{ RenderTree(depth + 1, maxDepth); }
        </FluentTreeItem>
    }
}
</FluentTreeView>

Here, the child content of FluentTreeItem should be compiled as a RenderFragment that acts on whatever RenderTreeBuilder is passed in. But because of C# scoping rules, the RenderFragment actually acts on the __builder captured from its parent context, so it is simply corrupting the output instead of doing something useful.

Possible solutions:

  1. We could ask for the Razor compiler block the use of local functions inside @{ ... } specifically. However that's probably impractical because Razor doesn't parse the contents of @{ ... }.
    • Perhaps it is achievable as an analyzer that acts on the code after the Razor compiler has generated it.
  2. We could do something in the runtime to detect more generally any cases where the wrong RenderTreeBuilder is invoked. For example if the runtime set an "rendering in progress" flag on it before it starts rendering and synchronously unsets that flag at the end of rendering, then it would have caught this case because child components are rendered afterwards (not recursively), so when the child is rendered it would see it's trying to write to a builder that does not have the "rendering in progress" flag set.
    • Drawback: how do we even check if this flag is set? We would not check it as part of each rendering instruction. There's plenty of evidence that rendering perf is sensitive to that kind of thing (and we can't just check it in development either).
    • Possible solution: instead of just setting a flag, actually null out the referencing to the underlying buffer (storing it in some other field to be swapped back later). Then if anyone tries to write to the builder while it's not marked as rendering-in-progress, they will get a NullReferenceException instead of corrupt output. Obviously that's not super easy to understand but avoids any perf cost.

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

I'm rendering a blazor wasm component using a recursive C# method and while it's working fine using C# only (OpenComponent, AddAttribute, etc.), it fails when returning HTML from that recursive method.

Expected Behavior

I'm expecting a tree structure to be built and clicking on a line should expand its children but it seems like the children are not rendered and an error is thrown on click.

Steps To Reproduce

I was not able to reproduce the issue without the library fluentui-blazor.

dotnet new install Microsoft.FluentUI.AspNetCore.Templates
dotnet new fluentblazorwasm --name aspnetcore-issue-53269
cd aspnetcore-issue-53269

Then replace Home.razor with

@page "/"

<PageTitle>Home</PageTitle>

<FluentTreeView>
@{
    RenderTree(0, 3);

    void RenderTree(int depth, int maxDepth)
    {
        if (depth >= maxDepth)
        {
            return;
        }

        <FluentTreeItem Text="item">
            @{ RenderTree(depth + 1, maxDepth); }
        </FluentTreeItem>
    }
}
</FluentTreeView>

Click on the item generated and check the error in the console.

It could be an error with the library (initially reported here microsoft/fluentui-blazor#1289) but this code works fine:

@page "/"

<PageTitle>Home</PageTitle>

<FluentTreeView>
    @RenderTree(0, 3)
</FluentTreeView>

@code {
    public static RenderFragment RenderTree(int depth, int maxDepth)
    {
        return builder =>
        {
            if (depth >= maxDepth)
            {
                return;
            }

            builder.OpenComponent<FluentTreeItem>(0);
            builder.AddAttribute(1, "Text", "item");
            builder.AddAttribute(2, "ChildContent", RenderTree(depth + 1, maxDepth));
            builder.CloseComponent();
        };
    }
}

Exceptions (if any)

blazor.webassembly.js:1 crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRendere...

</details>
Fixes dotnet/aspnetcore#53269

<!-- START COPILOT CODING AGENT TIPS -->
---

✨ Let Copilot coding agent [set things up for you](https://github.com/dotnet/aspnetcore/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo.

Copy link
Contributor

Greetings human! You've submitted a PR that modifies code that is shared with https://github.com/dotnet/runtime . Please make sure you synchronize this code with the changes in that repo!

@Copilot Copilot AI changed the title [WIP] Prevent use of local functions inside markup Add analyzer to prevent local functions accessing RenderTreeBuilder from parent scope Sep 25, 2025
Copilot finished work on behalf of javiercn September 25, 2025 12:29
@Copilot Copilot AI requested a review from javiercn September 25, 2025 12:29
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.

Prevent use of local functions inside markup
2 participants