Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ section is the GitHub release's notes.
- AspNetCore: `FaultResponse.WithoutFaultName()` answers a member without naming it: no `fault`, no `rule` and no member description in the problem, so two causes on one status look alike; the document says that status names no fault, and `IFaultFeature.NameWithheld` tells the host ([Faults a client must not tell apart](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.AspNetCore/README.md#faults-a-client-must-not-tell-apart)).
- `Result.CatchAsync` takes a call that returns a `ValueTask`, `stream.ReadAsync(buffer)` without `.AsTask()`; an `async` lambda binds as before ([An exception as an outcome](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/docs/result.md#an-exception-as-an-outcome)).
- `TraverseAsync` on `Option<T>` and `Patch<T>`: an asynchronous check of an optional input or a patch field, with a token form; nothing is called without a value ([Collect many results](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/docs/result.md#collect-many-results)).
- `Choose()`, `Choose(selector)`, `SequenceAsync` and `TraverseAsync` read an `IAsyncEnumerable<T>`, in order and without a list first; the token reaches the stream, nothing is read after the first error, and the enumerator is disposed however they stop ([Chain steps](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit/README.md#chain-steps)).
- `Fault<TFault>.Cause`: `Fault.Of(reason, detail, exception)` keeps the exception underneath for your log; equality and `ToString()` leave it out, and `ToHttpResult()` hands it to the host as `IFaultFeature.Cause` and `FaultProblem<TFault>.Cause`, never into the problem ([What failed underneath](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.AspNetCore/README.md#what-failed-underneath)).
- Testing: every assertion works on a `Task` or `ValueTask` of what it checks, `await service.FindAsync(id).ShouldBeSome()`; a task that throws fails the test with its own exception, and an assertion left without `await` is reported (CMTK0003) ([CodoMetis.TypeKit.Testing](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/src/CodoMetis.TypeKit.Testing/README.md)).
- OpenAPI: `AddTypeKit(typeKit => typeKit.InlineValueObjects = true)` describes every value object in place by its underlying type's schema, in every position and in OpenAPI 3.1, 3.0 and 2.0, with no component of its own; closed sets keep theirs, and without the setting the document is unchanged ([Describe value objects as plain types](https://github.com/CaffeinatedCoder/CodoMetis.TypeKit/blob/main/docs/aspnet-core.md#describe-value-objects-as-plain-types)).
Expand Down
14 changes: 13 additions & 1 deletion design/option-result.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,19 @@ is the value-less shape. Anything that turns this into a second LanguageExt is o
once on it, so in a UI or a Blazor component a callback that touches dispatcher-bound state works or
fails by timing. That is documented rather than changed: await the task and call the synchronous
combinator, or marshal inside the callback. `AsynchronousContextTests` holds every public member
that returns a task to it.
that returns a task or an async stream to it.
- **Async streams (2.3.0, #128).** `Choose()` and `Choose(selector)` over an `IAsyncEnumerable<T>`
return one, lazily, the caller's token reaching the source through `[EnumeratorCancellation]`.
`SequenceAsync(cancellationToken)` and `TraverseAsync` over one read the next element only after the
one before it succeeded, check a token they are given before each read and each operation, and
dispose the enumerator on every exit. Measured 2026-10-29 against .NET 10's
`System.Linq.AsyncEnumerable` and EF Core 10.0.12: no name binds ambiguously beside its operators,
and a `T[]` or a `DbSet<T>` (which EF Core 10 does not convert to `IAsyncEnumerable<T>`) still binds
to the sequence members. A type that implements both `IEnumerable<T>` and `IAsyncEnumerable<T>` is
ambiguous (CS0121) for `Choose(selector)` and `TraverseAsync`, as it is for the BCL's own `Where`. That
is left as the BCL leaves it, not settled by an overload priority: a silent pick of either reading is
worse than a compile error. CMTK0003 reports a dropped `SequenceAsync` by its type, as it does
`TraverseAsync`, with no change to the analyzer.

**Decided 2026-09-27: an uninitialized `Result` throws.**
- A `default` result is `State == Uninitialized`. If the branching members tested only
Expand Down
1 change: 1 addition & 0 deletions docs/option.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ elements `Choose(selector)` reads may be null, and only what the selector finds
| `GetValueOrNone(key)` | `IReadOnlyDictionary<TKey, TValue>` | `Option<TValue>` |
| `Choose(selector)` | `IEnumerable<T>`, its elements nullable or not | the values the selector answers `Some` for, in order and lazily: a filter and a map in one |
| `Choose()` | `IEnumerable<Option<T>>` | the values of the options that hold one, in order and lazily |
| `Choose(selector)`, `Choose()` | `IAsyncEnumerable<T>`, `IAsyncEnumerable<Option<T>>` | the same, as an `IAsyncEnumerable` of the values as they arrive; the token given to `WithCancellation` reaches the stream |
| `IsSome()`, `IsNone()` | `Option<T>` | `bool` |
| `Match(onSome, onNone)` | `Option<T>` | what the branch returns |
| `TryGetValue(out value)` | `Option<T>` | `bool`; `value` is non-null when true |
Expand Down
2 changes: 2 additions & 0 deletions docs/result.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,9 +323,11 @@ throughout.
| `Traverse(selector)` | `Option<T>` | `Result<Option<TResult>, TError>`: an optional input validated when present |
| `Traverse(selector)` | `Patch<T>` | `Result<Patch<TResult>, TError>`: a patch field validated when it carries a value; a clear or an absence passes through |
| `TraverseAsync(selector)`, `TraverseAsync(selector, cancellationToken)` | `IEnumerable<T>` | as `Traverse`, one operation awaited at a time; nothing starts after an error, or once the token is cancelled |
| `TraverseAsync(selector)`, `TraverseAsync(selector, cancellationToken)` | `IAsyncEnumerable<T>` | as over `IEnumerable<T>`; nothing more is read after an error, and the token reaches the stream too |
| `TraverseAsync(selector)`, `TraverseAsync(selector, cancellationToken)` | `Option<T>`, `Patch<T>` | as `Traverse`, the operation awaited; nothing is called without a value |
| `Transpose()` | `Option<Result<T, TError>>`, `Result<Option<T>, TError>` | the one turned inside out: `None` is a success without a value, an error stays the error |
| `Sequence()` | `IEnumerable<Result<T, TError>>`, `IEnumerable<Result<TError>>` | the list of values, or the first error |
| `SequenceAsync()`, `SequenceAsync(cancellationToken)` | `IAsyncEnumerable<Result<T, TError>>`, `IAsyncEnumerable<Result<TError>>` | the same, awaited; nothing more is read after the first error, and the token reaches the stream |

**Reaching the outcome**

Expand Down
27 changes: 27 additions & 0 deletions src/CodoMetis.TypeKit/OptionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,24 @@ public IEnumerable<T> Choose()
}
}

/// <param name="options">The stream of options.</param>
/// <typeparam name="T">The type of the values.</typeparam>
extension<T>(IAsyncEnumerable<Option<T>> options) where T : notnull
{
/// <summary>
/// The values of the options that hold one, in order, as they arrive: a stream of lookups to what
/// was found. Lazy, as an async LINQ operator is, and the token given to <c>WithCancellation</c> or
/// <c>GetAsyncEnumerator</c> reaches the stream it reads.
/// </summary>
/// <returns>The values, in order.</returns>
public IAsyncEnumerable<T> Choose()
{
ArgumentNullException.ThrowIfNull(options);

return Chosen(options, static option => option);
}
}

/// <param name="instance">The nullable value.</param>
/// <typeparam name="T">The underlying value type.</typeparam>
extension<T>(in T? instance) where T : struct
Expand Down Expand Up @@ -679,4 +697,13 @@ internal static IEnumerable<TResult> Chosen<T, TResult>(IEnumerable<T> source, F
if (selector(element).TryGetValue(out var value)) yield return value;
}
}

/// <summary>The iterator behind both <c>Choose</c> over a stream; the caller's token is the stream's.</summary>
internal static async IAsyncEnumerable<TResult> Chosen<T, TResult>(IAsyncEnumerable<T> source, Func<T, Option<TResult>> selector, [EnumeratorCancellation] CancellationToken cancellationToken = default) where TResult : notnull
{
await foreach (var element in source.WithCancellation(cancellationToken).ConfigureAwait(false))
{
if (selector(element).TryGetValue(out var value)) yield return value;
}
}
}
15 changes: 15 additions & 0 deletions src/CodoMetis.TypeKit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ Option<decimal> discount = // a later from
- **`Choose(selector)`** keeps the values a selector returns `Some` for, in order and without the
`None`s. It filters and maps in one step, over elements that may be null
(`nicknames.Choose(n => n.ToOption())`). `Choose()` keeps the values a sequence of options holds.
Both read an async stream too, as its elements arrive.

On a database query, `FirstOrNone`, `FirstOrError`, `SingleOrNone` and `ElementAtOrNone` ask the
provider for only the element they need, and let it run the predicate (a `WHERE` and a `LIMIT` in
Expand Down Expand Up @@ -226,6 +227,20 @@ sequence order. `Traverse` calls nothing after it. None of them collects errors.
cancellation token you give it is cancelled. For operations that may all run at once, use
`Task.WhenAll` and `Sequence()`.

An async stream (`IAsyncEnumerable<T>`), such as a paged API or a query read with
`AsAsyncEnumerable()`, needs no list first:

```csharp
IAsyncEnumerable<Sku> known = lines.Choose(line => Sku.TryFrom(line)); // the lines that are SKUs, as they arrive
Result<IReadOnlyList<Sku>, SkuFault> imported = await rows.SequenceAsync(cancellationToken); // every SKU, or the first fault
Result<IReadOnlyList<Order>, OrderFault> found = await ids.TraverseAsync(orders.FindAsync); // one lookup at a time, in order
```

`SequenceAsync` and `TraverseAsync` stop at the first error and read nothing after it. All three read
the stream in order, and dispose its enumerator however they stop: at an error, at a `break`, or when
your selector throws. The token you give `SequenceAsync` or `TraverseAsync` reaches the stream. For
`Choose`, pass yours to `WithCancellation` on what it returns.

The asynchronous steps take callbacks that return a `Task`, and wait for them.
`.TapErrorAsync(fault => audit.RecordAsync(fault))` has recorded the fault when the chain's `await`
returns, and what it throws comes out of that `await`.
Expand Down
Loading
Loading