Repository navigation
feat: Choose, SequenceAsync and TraverseAsync read an async stream - #139
Merged
Merged
Conversation
Choose() and Choose(selector) over an IAsyncEnumerable<T> return one, lazily, the caller's token reaching the source through [EnumeratorCancellation]. SequenceAsync(cancellationToken) over a stream of either Result shape, and the four TraverseAsync overloads over a stream, read the next element only after the one before succeeded, check a token they are given before each read and each operation, and dispose the enumerator on every exit. Measured before the code (design/option-result.md): no name binds ambiguously beside .NET 10's System.Linq.AsyncEnumerable, and a T[] or an EF Core 10 DbSet<T> still binds to the sequence members; a type that is both IEnumerable<T> and IAsyncEnumerable<T> is ambiguous for Choose(selector) and TraverseAsync as it is for the BCL's own Where. CMTK0003 reports a dropped SequenceAsync or stream TraverseAsync by its type with no analyzer change; the new analyzer test pins it. Variants: each behaviour runs over a stream whose elements arrive at once and one whose elements each wait, nullable and non-nullable elements, both Result shapes, both token forms. The completeness tests (NullDelegateTests, UninitializedResultTests, AsynchronousContextTests, the last widened to members returning an async stream) took the new members. Thirteen seeded defects in src/ (a token not passed, a check dropped, ConfigureAwait(false) dropped, an enumerator left undisposed, a selector checked late) each fail a test; the stream TraverseAsync's context case first missed one and now reads a pending stream. Closes #128 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tream-option-result # Conflicts: # test/oracles/claims.json
…tream-option-result # Conflicts: # test/oracles/claims.json # test/oracles/surface-waivers.json
This was referenced Oct 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #128
Problem
Chooseand everyTraverseAsynctook anIEnumerable<T>, and nothing inCodoMetis.TypeKittook anIAsyncEnumerable<T>. A paged API, a streamed import or an EF query read withAsAsyncEnumerable()had to be buffered into a list first, or unwrapped by hand withTryGetValuein anawait foreach.Change
OptionExtensions.cs,SequenceOptionExtensions.cs:Choose()overIAsyncEnumerable<Option<T>>andChoose(selector)overIAsyncEnumerable<T>. Both are lazy and keep order, and both check their arguments at the call. The token fromWithCancellation/GetAsyncEnumeratorreaches the source ([EnumeratorCancellation]).ResultExtensions.cs:SequenceAsync(cancellationToken = default)over a stream ofResult<T, TError>and ofResult<TError>, plus the fourTraverseAsyncoverloads overIAsyncEnumerable<T>, the same overloads as the sequence ones. They read the next element only after the one before it succeeded. A token they are given goes to the enumerator and is checked before each read and each operation. The enumerator is disposed on every exit. Every await isConfigureAwait(false).design/option-result.md):System.Linq.AsyncEnumerable.T[]and EF Core 10.0.12'sDbSet<T>, which has noIAsyncEnumerable<T>conversion, still bind to the sequence members.IEnumerable<T>andIAsyncEnumerable<T>is CS0121 forChoose(selector)andTraverseAsync, as it already is for the BCL'sWhere. I left it at that rather than add an overload priority, since silently picking either reading is worse than a compile error.SequenceAsyncor streamTraverseAsyncby its return type: awaited or not, and converted to a plainTask. There is no analyzer change; a new test pins it.streamssample and a paragraph.docs/option.mdanddocs/result.mdsurface tables have rows.test/oracles: fiveA01-*claims for the new README sentences, held byAsyncStreamTests. Since test: read the unread documents for claims #140 removed the blanketOption.*/Result.*waivers, they are what holdsResult.SequenceAsync, and they join test: read the unread documents for claims #140's claims onChooseandTraverseAsync. No waiver names a new member.Tests
AsyncStreamTestsruns over a stream whose elements arrive at once and over one whose elements each wait. It checks:defaultoptions act asNone, the same as over a sequence;WithCancellation,GetAsyncEnumeratorand the token parameter;break, and when the selector throws;Resultis refused;NullDelegateTests,NullTaskTests,UninitializedResultTestsandAsynchronousContextTestscover the new members.AsynchronousContextTests' completeness check now also takes members that return anIAsyncEnumerable.IgnoredOutcomeAnalyzerTests.A_dropped_SequenceAsync_or_TraverseAsync_over_a_stream_reports_as_over_a_sequence.Seeds: 13 defects seeded in
src/one at a time, each caught by a failing test:ConfigureAwait(false)dropped from each member;The stream
TraverseAsynccontext case first missedConfigureAwait(false), because its stream completed at once. It now reads a stream whose first element is still pending.Local runs:
dotnet packof the core package passes package validation against the 2.2.0 baseline.🤖 Generated with Claude Code