Skip to content

Generate a real async iterator for IAsyncEnumerable actions #13187 - #13191

Merged
yasmoradi merged 4 commits into
bitfoundation:developfrom
yasmoradi:fix/http-client-proxy-async-enumerable
Sep 10, 2026
Merged

Generate a real async iterator for IAsyncEnumerable actions #13187#13191
yasmoradi merged 4 commits into
bitfoundation:developfrom
yasmoradi:fix/http-client-proxy-async-enumerable

Conversation

@yasmoradi

@yasmoradi yasmoradi commented Sep 9, 2026

Copy link
Copy Markdown
Member

Closes #13187.

An IAsyncEnumerable<T> action generated a proxy that did not compile (CS1622): an async method with the iterator's return type and a return statement, wrapped in prerenderStateService.GetValue — which stores one resolved value per url and cannot hold a stream.

The streamed path is now a real async iterator:

public async IAsyncEnumerable<string> PerformDiagnostic(..., [EnumeratorCancellation] CancellationToken cancellationToken)
{
    using var __request = new HttpRequestMessage(HttpMethod.Get, __url);
    ...
    using var __response = await httpClient.SendAsync(__request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
    await foreach (var __item in __response.Content.ReadFromJsonAsAsyncEnumerable(options.GetTypeInfo<string>(), cancellationToken))
    {
        yield return __item;
    }
}
  • yields the items through instead of returning them
  • skips the prerender state for this shape only
  • [EnumeratorCancellation] on the token, so a WithCancellation at the call site reaches the enumerator
  • the response's using moves inside the iterator, which makes WrapWithResponseDisposal redundant — removed
  • the JsonTypeInfo is still passed, so trimming and AOT are unaffected

Every other action is generated exactly as before.

Verified by building the generator and pointing Boilerplate.Shared at it in place of the package, with IDiagnosticController.PerformDiagnostic declared as IAsyncEnumerable<string>: the project builds, the emitted method is the one above, and the other 62 generated methods still go through prerenderStateService unchanged.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved generated HTTP client proxies for streaming responses returned as asynchronous streams.
    • Streamed items are now delivered incrementally without unnecessary response wrapping.
    • Cancellation requests are now correctly propagated while consuming asynchronous streams.
    • Streaming responses remain available for the duration of enumeration, preventing premature disposal.

yasmoradi and others added 4 commits September 9, 2026 16:03
…erable actions

A controller action returning IAsyncEnumerable<T> produced a proxy that did not
compile: the method came out `async` with the iterator's return type and a
`return` statement (CS1622), wrapped in prerenderStateService.GetValue, which
stores one resolved value per url and cannot hold a stream.

The streamed path now yields the items through, keeps the request and response
alive for the enumeration with `using`, skips the prerender state, and marks the
cancellation token with EnumeratorCancellation so a WithCancellation at the call
site reaches the enumerator. WrapWithResponseDisposal goes with it - the
response's `using` now lives inside the iterator.

Every other action is generated exactly as before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…or path

doesReturnIAsyncEnumerable is computed from the unwrapped return type, so
Task<IAsyncEnumerable<T>> and ValueTask<IAsyncEnumerable<T>> set it too - and a
method declared Task<...> cannot be an iterator. Only a method whose own return
type is the stream takes the new path; the wrapped shapes keep the behaviour
they had, which is why WrapWithResponseDisposal comes back.

Verified against Boilerplate with the three shapes: Task<string[]>,
IAsyncEnumerable<string> and Task<IAsyncEnumerable<string>> all build, and each
generates what it should.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e encoded

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@yasmoradi
yasmoradi requested a review from msynk September 9, 2026 14:59
@coderabbitai

coderabbitai Bot commented Sep 9, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: d4832af7-eea3-4423-9b78-282ef36a4e36

📥 Commits

Reviewing files that changed from the base of the PR and between 7b52dbe and 6d25edb.

📒 Files selected for processing (1)
  • src/SourceGenerators/Bit.SourceGenerators/HttpClientProxy/HttpClientProxySourceGenerator.cs

Included review availability: Your plan provides up to 4 included reviews per hour; 1 remains after this review.


Walkthrough

The source generator now identifies direct IAsyncEnumerable<T> methods separately from task-wrapped streams. It generates iterator-compatible proxy methods with enumerator cancellation and stream-owned response disposal.

Changes

Async stream proxy generation

Layer / File(s) Summary
Async stream classification and encoding
src/SourceGenerators/Bit.SourceGenerators/HttpClientProxy/HttpClientProxySourceGenerator.cs
The generator records whether an action directly returns IAsyncEnumerable<T>. It encodes and parses the new action field.
Async stream method generation
src/SourceGenerators/Bit.SourceGenerators/HttpClientProxy/HttpClientProxySourceGenerator.cs
Direct async streams receive [EnumeratorCancellation], use await foreach, skip prerender-state wrapping, and keep the response undisposed while the stream is consumed.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 6d25e

Direct IAsyncEnumerable proxy actions now compile and stream response items while keeping the response alive through enumeration. No concrete merge-blocking risk remains.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the primary change: generating a real async iterator for direct IAsyncEnumerable actions. It directly matches issue #13187.
Linked Issues check ✅ Passed The changes address issue #13187 by generating a valid async iterator for direct IAsyncEnumerable returns, using streaming response handling, applying EnumeratorCancellation, preserving response li…
Out of Scope Changes check ✅ Passed The changes are limited to the HttpClientProxy source generator behavior required for direct IAsyncEnumerable actions. No unrelated code changes are identified.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 1 files.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

A rabbit reads each line,
The patch grows clear beneath the moon,
Small changes hop in place,
Tests guard the garden path,
Reviews bloom before the dawn.

Comment @coderabbitai help to get the list of available commands.

@yasmoradi
yasmoradi merged commit a27077c into bitfoundation:develop Sep 10, 2026
3 checks passed
@yasmoradi
yasmoradi deleted the fix/http-client-proxy-async-enumerable branch September 10, 2026 10:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HttpClientProxy source generator emits uncompilable code for IAsyncEnumerable actions

1 participant