Skip to content

feat: Choose, SequenceAsync and TraverseAsync read an async stream - #139

Merged
CaffeinatedCoder merged 3 commits into
release/2.3.0from
feat/async-stream-option-result
Oct 29, 2026
Merged

CaffeinatedCoder merged 3 commits into
release/2.3.0from
feat/async-stream-option-result

Conversation

@CaffeinatedCoder

@CaffeinatedCoder CaffeinatedCoder commented Oct 29, 2026 •

Copy link
Copy Markdown
Owner

Closes #128

Problem

Choose and every TraverseAsync took an IEnumerable<T>, and nothing in CodoMetis.TypeKit took an IAsyncEnumerable<T>. A paged API, a streamed import or an EF query read with AsAsyncEnumerable() had to be buffered into a list first, or unwrapped by hand with TryGetValue in an await foreach.

Change

  • OptionExtensions.cs, SequenceOptionExtensions.cs: Choose() over IAsyncEnumerable<Option<T>> and Choose(selector) over IAsyncEnumerable<T>. Both are lazy and keep order, and both check their arguments at the call. The token from WithCancellation/GetAsyncEnumerator reaches the source ([EnumeratorCancellation]).
  • ResultExtensions.cs: SequenceAsync(cancellationToken = default) over a stream of Result<T, TError> and of Result<TError>, plus the four TraverseAsync overloads over IAsyncEnumerable<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 is ConfigureAwait(false).
  • Name check, done before the code (recorded in design/option-result.md):
    • No name binds ambiguously beside .NET 10's System.Linq.AsyncEnumerable.
    • T[] and EF Core 10.0.12's DbSet<T>, which has no IAsyncEnumerable<T> conversion, still bind to the sequence members.
    • A type implementing both IEnumerable<T> and IAsyncEnumerable<T> is CS0121 for Choose(selector) and TraverseAsync, as it already is for the BCL's Where. I left it at that rather than add an overload priority, since silently picking either reading is worse than a compile error.
  • CMTK0003 already reports a dropped SequenceAsync or stream TraverseAsync by its return type: awaited or not, and converted to a plain Task. There is no analyzer change; a new test pins it.
  • Docs:
    • The core README's "Chain steps" has a compiled streams sample and a paragraph.
    • The Option section has one sentence.
    • The docs/option.md and docs/result.md surface tables have rows.
    • The changelog has a line under 2.3.0.
  • test/oracles: five A01-* claims for the new README sentences, held by AsyncStreamTests. Since test: read the unread documents for claims #140 removed the blanket Option.*/Result.* waivers, they are what holds Result.SequenceAsync, and they join test: read the unread documents for claims #140's claims on Choose and TraverseAsync. No waiver names a new member.

Tests

  • AsyncStreamTests runs over a stream whose elements arrive at once and over one whose elements each wait. It checks:

    • order is kept;
    • default options act as None, the same as over a sequence;
    • null elements are accepted;
    • reading is lazy;
    • the token reaches the enumerator through WithCancellation, GetAsyncEnumerator and the token parameter;
    • cancellation mid-stream stops reading, including from a stream that ignores its token;
    • a cancelled token reads nothing;
    • the enumerator is disposed on the first error, on a break, and when the selector throws;
    • an uninitialized Result is refused;
    • a consumer compilation beside the async LINQ operators has no warning or ambiguity.
  • NullDelegateTests, NullTaskTests, UninitializedResultTests and AsynchronousContextTests cover the new members. AsynchronousContextTests' completeness check now also takes members that return an IAsyncEnumerable.

  • 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:

    • a token not passed to the stream;
    • each of the cancellation checks dropped;
    • ConfigureAwait(false) dropped from each member;
    • the enumerator left undisposed;
    • the selector checked late.

    The stream TraverseAsync context case first missed ConfigureAwait(false), because its stream completed at once. It now reads a stream whose first element is still pending.

  • Local runs:

    • full solution through build-verdict: 18,547 passed, 471 skipped;
    • dotnet pack of the core package passes package validation against the 2.2.0 baseline.

🤖 Generated with Claude Code

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>
@CaffeinatedCoder CaffeinatedCoder added this to the 2.3.0 milestone Oct 29, 2026
@CaffeinatedCoder CaffeinatedCoder added enhancement New feature or request package/core Affects CodoMetis.TypeKit area/option-result Option, Result and their extensions labels Oct 29, 2026
…tream-option-result

# Conflicts:
#	test/oracles/claims.json
…tream-option-result

# Conflicts:
#	test/oracles/claims.json
#	test/oracles/surface-waivers.json
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/option-result Option, Result and their extensions enhancement New feature or request package/core Affects CodoMetis.TypeKit

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant