Skip to content

Fix BL0012 false positive on await foreach and await using - #68720

Open
AzeemullahRg wants to merge 1 commit into
dotnet:mainfrom
AzeemullahRg:fix/bl0012-statement-await-forms
Open

Fix BL0012 false positive on await foreach and await using#68720
AzeemullahRg wants to merge 1 commit into
dotnet:mainfrom
AzeemullahRg:fix/bl0012-statement-await-forms

Conversation

@AzeemullahRg

@AzeemullahRg AzeemullahRg commented Aug 22, 2026

Copy link
Copy Markdown

Problem

BL0012 reports StateHasChanged calls that are required, whenever the enclosing method's only awaits are await foreach or await using.

protected override async Task OnInitializedAsync()
{
    await foreach (var item in LoadAsync())
    {
        _items.Add(item);
        StateHasChanged();   // BL0012: "unnecessary in method 'OnInitializedAsync'"
    }
}

The call is what paints each item as it arrives. Accepting the offered code fix removes it, and the list then stays empty until the stream completes and appears in one batch.

Cause

StateHasChangedAnalyzer located the first and last await by collecting AwaitExpressionSyntax nodes:

var awaitExpressions = body.DescendantNodes(...).OfType<AwaitExpressionSyntax>()

Three forms do not produce an AwaitExpressionSyntax. They carry their await as a keyword token on the statement instead:

Form Token
await foreach (...) CommonForEachStatementSyntax.AwaitKeyword
await using (...) { } UsingStatementSyntax.AwaitKeyword
await using var x = ...; LocalDeclarationStatementSyntax.AwaitKeyword

A method whose only awaits take one of those forms therefore looked await-free. The analyzer took its Count == 0 branch, which treats every call in the method as redundant on the grounds that ComponentBase renders once the method returns.

Fix

Track the region the method can suspend in, rather than a set of await positions. Each construct contributes the span over which it can yield:

Form Suspends Contributes
await expr at the expression the expression
await foreach (...) on each iteration, and when the enumerator is disposed the statement
await using (...) { } when the resource is disposed, at the end of the statement the statement
await using var x = ...; when the enclosing block exits to the end of that block

The reporting rule is unchanged: a call is redundant when it sits outside that region, so behaviour for ordinary awaits stays as it was.

The last row is why a call after an await using declaration is no longer reported. Its disposal runs at the end of the enclosing block, so the call still has an await after it and ComponentBase has not rendered yet.

Tests

Five added to StateHasChangedAnalyzerTest:

  • three asserting no diagnostic for a call inside await foreach, inside an await using block, and after an await using declaration
  • two asserting a call before or after an await foreach is still reported

With the analyzer change reverted the three fail and the two pass.

Found while validating #68484.

@AzeemullahRg
AzeemullahRg requested a review from a team as a code owner August 22, 2026 20:05
@dotnet-policy-service dotnet-policy-service Bot added the community-contribution Indicates that the PR has been added by a community member label Aug 22, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Thanks for your PR, @AzeemullahRg. Someone from the team will get assigned to your PR shortly and we'll get it reviewed.

@AzeemullahRg
AzeemullahRg force-pushed the fix/bl0012-statement-await-forms branch from a4adc00 to 362a078 Compare August 22, 2026 20:16
StateHasChangedAnalyzer located the first and last await by collecting
AwaitExpressionSyntax nodes. `await foreach`, `await using (...) { }` and
`await using var x = ...;` carry their await as a keyword token on the statement, so a
method whose only awaits take one of those forms looked await-free and every
StateHasChanged in it was reported as redundant.

Track the region the method can suspend in instead, taking the statement span for
`await foreach` and for the block form of `await using`, and the enclosing block for a
using declaration, whose disposal runs when that block exits. Calls outside that region
are still reported, so behaviour for ordinary awaits is unchanged.

Found while validating dotnet#68484.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018irsMDStKQdmB1k7fkUmqM
@AzeemullahRg
AzeemullahRg force-pushed the fix/bl0012-statement-await-forms branch from 362a078 to ef44591 Compare August 22, 2026 20:18
@AzeemullahRg

Copy link
Copy Markdown
Author

@dotnet-policy-service agree

@AzeemullahRg

Copy link
Copy Markdown
Author

CI triage: both failures are pre-existing flakes, unrelated to this change

This PR only touches src/Components/Analyzers/src/StateHasChangedAnalyzer.cs and StateHasChangedAnalyzerTest.cs. All analyzer tests passed. Neither failing test is in that area.

1. ServerVirtualizationTest.QuickGrid_InitialIndex_TallContainer_NearEnd_FillsViewportWithoutUserScroll(useProvider: False)aspnetcore-components-e2e / Blazor E2E (Mono)

Xunit.Sdk.TrueException: Item 950 should remain aligned with the viewport top
once the last item has loaded, but top rendered index was -1, scrollTop=0.

Build Analysis matched this to Known Build Error #68708 (same "top rendered index was -1" assertion). There is also an open quarantine request for this exact test: #68724.

2. Identity.EntityFrameworkCore.Test.ProtectedUserStoreTest.LoadFromDbFindByEmailTestaspnetcore-ci / Helix x64 Subset 1 - ubuntu.2404.amd64.open

System.ObjectDisposedException : Cannot access a disposed object.
Object name: 'SQLitePCL.sqlite3'
   at SqliteConnection.Open()
   ...
   at SqlStoreTestBase`3.LazyLoadTestSetup(TestDbContext db, TUser user)

A SQLite connection torn down under a concurrent test, not a product failure. Build Analysis reports a 0.84% historical failure rate for this test. It is not matched to a Known Build Error issue, so it shows as unmatched. 1 failure out of 85,417 tests in that build.

Ask: could a maintainer re-run the two failed legs? I don't have permission to re-queue dnceng-public builds. Happy to push an empty commit instead if that's preferred.

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

Labels

community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant