Skip to content

[Blazor] Add Razor analyzer to warn when for-loop iterator variable is captured in lambdas, EventCallbacks, or data binding expressions #65234

@javiercn

Description

@javiercn

Summary

Add a Roslyn analyzer to detect and warn when a for loop iterator variable is captured in a lambda expression, EventCallback, two-way binding (@bind), or other closure context within Razor components. This is a common pitfall that leads to unexpected runtime behavior because all closures reference the same variable, which holds the final value after the loop completes.

Cases Where the Analyzer SHOULD Warn ⚠️

1. Lambda expressions in event handlers capturing for loop variable

@for (var i = 0; i < items.Length; i++)
{
    <button @onclick="@(() => SelectItem(i))">Item @i</button>
}

2. Two-way binding with @bind using loop variable as indexer

@for (var i = 0; i < stringArray.Length; i++)
{
    <input type="text" @bind="stringArray[i]" />
}

3. EventCallback parameters with captured loop variable

@for (var i = 0; i < 5; i++)
{
    <MyComponent OnClick="@(() => HandleClick(i))" />
}

4. RenderFragment/ChildContent capturing loop variable

@for (var i = 0; i < 5; i++)
{
    <MyComponent>Count: @i</MyComponent>
}

5. Lambda with event args still capturing loop variable

@for (var i = 1; i < 4; i++)
{
    <button @onclick="@(e => UpdateHeading(e, i))">Button #@i</button>
}

Cases Where the Analyzer Should NOT Warn ✅

1. Local variable copy within the loop body

@for (var i = 0; i < items.Length; i++)
{
    var index = i;
    <button @onclick="@(() => SelectItem(index))">Item @index</button>
}

2. Using foreach (iteration variable is scoped per iteration in C# 5.0+)

@foreach (var item in items)
{
    <button @onclick="@(() => SelectItem(item))">@item.Name</button>
}

3. Using foreach with Enumerable.Range

@foreach (var i in Enumerable.Range(0, items.Length))
{
    <button @onclick="@(() => SelectItem(i))">Item @i</button>
}

4. Using Index() LINQ method (.NET 9+) for index and value

@foreach (var (index, item) in items.Index())
{
    <button @onclick="@(() => SelectItem(index))">@item.Name</button>
}

5. Using Select with index overload

@foreach (var entry in items.Select((item, index) => (item, index)))
{
    <button @onclick="@(() => SelectItem(entry.index))">@entry.item.Name</button>
}

6. Direct method reference (no lambda/closure)

@for (var i = 0; i < items.Length; i++)
{
    <button @onclick="HandleClick">Item @i</button>
}

7. Loop variable used only in non-closure contexts (display only)

@for (var i = 0; i < items.Length; i++)
{
    <span>Item @i</span>  // No closure, just rendering
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    analyzerIndicates an issue which is related to analyzer experiencearea-blazorIncludes: Blazor, Razor Components

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions