From 349480fe4a3682a7afa3b09fe4aa79d0006aa092 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 02:35:32 +0000 Subject: [PATCH 1/3] Initial plan From 245abd11b02cc62c899498ef206ce42fda4e01c7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 02:41:30 +0000 Subject: [PATCH 2/3] feat: add Else overloads for FailOr Co-authored-by: mark-pro <20671988+mark-pro@users.noreply.github.com> --- src/FailOr/FailOrT.Else.cs | 262 ++++++++++++ tests/FailOr.Tests/ElseTestData.cs | 460 ++++++++++++++++++++++ tests/FailOr.Tests/FailOrElseTests.cs | 85 ++++ tests/FailOr.Tests/TaskFailOrElseTests.cs | 114 ++++++ 4 files changed, 921 insertions(+) create mode 100644 src/FailOr/FailOrT.Else.cs create mode 100644 tests/FailOr.Tests/ElseTestData.cs create mode 100644 tests/FailOr.Tests/FailOrElseTests.cs create mode 100644 tests/FailOr.Tests/TaskFailOrElseTests.cs diff --git a/src/FailOr/FailOrT.Else.cs b/src/FailOr/FailOrT.Else.cs new file mode 100644 index 0000000..6176bb5 --- /dev/null +++ b/src/FailOr/FailOrT.Else.cs @@ -0,0 +1,262 @@ +namespace FailOr; + +/// +/// Provides terminal fallback extensions for values. +/// +public static class FailOrElseExtensions +{ + extension(FailOr source) + { + /// + /// Returns the successful value unchanged, or the provided alternative when the source is failed. + /// + /// The fallback value to return when the source is failed. + /// The wrapped success value, or when the source is failed. + /// + /// + /// var value = result.Else(42); + /// + /// + public TSource Else(TSource alternative) => + source.IsSuccess ? source.UnsafeUnwrap() : alternative; + + /// + /// Returns the successful value unchanged, or invokes a fallback factory when the source is failed. + /// + /// The fallback factory to invoke when the source is failed. + /// The wrapped success value, or the value produced by when the source is failed. + /// + /// Thrown when is . + /// + /// + /// + /// var value = result.Else(() => 42); + /// + /// + public TSource Else(Func alternative) + { + ArgumentNullException.ThrowIfNull(alternative); + + return source.IsSuccess ? source.UnsafeUnwrap() : alternative(); + } + + /// + /// Returns the successful value unchanged, or invokes a fallback factory with the source failures when the source is failed. + /// + /// The fallback factory to invoke with the source failures. + /// The wrapped success value, or the value produced by when the source is failed. + /// + /// Thrown when is . + /// + /// + /// + /// var value = result.Else(failures => failures.Count); + /// + /// + public TSource Else(Func, TSource> alternative) + { + ArgumentNullException.ThrowIfNull(alternative); + + return source.IsSuccess ? source.UnsafeUnwrap() : alternative(source.Failures); + } + + /// + /// Returns the successful value unchanged, or asynchronously invokes a fallback factory when the source is failed. + /// + /// The asynchronous fallback factory to invoke when the source is failed. + /// + /// A task producing the wrapped success value, or the value produced by when the source is failed. + /// + /// + /// Thrown when is or returns . + /// + /// + /// + /// var value = await result.ElseAsync(() => Task.FromResult(42)); + /// + /// + public Task ElseAsync(Func> alternativeAsync) + { + ArgumentNullException.ThrowIfNull(alternativeAsync); + + return source.IsSuccess + ? Task.FromResult(source.UnsafeUnwrap()) + : ElseAsyncCore(alternativeAsync); + } + + /// + /// Returns the successful value unchanged, or asynchronously invokes a fallback factory with the source failures when the source is failed. + /// + /// The asynchronous fallback factory to invoke with the source failures. + /// + /// A task producing the wrapped success value, or the value produced by when the source is failed. + /// + /// + /// Thrown when is or returns . + /// + /// + /// + /// var value = await result.ElseAsync(failures => Task.FromResult(failures.Count)); + /// + /// + public Task ElseAsync( + Func, Task> alternativeAsync + ) + { + ArgumentNullException.ThrowIfNull(alternativeAsync); + + return source.IsSuccess + ? Task.FromResult(source.UnsafeUnwrap()) + : ElseAsyncCore(source.Failures, alternativeAsync); + } + } + + extension(Task> sourceTask) + { + /// + /// Returns the awaited successful value unchanged, or the provided alternative when the awaited source is failed. + /// + /// The fallback value to return when the awaited source is failed. + /// + /// A task producing the wrapped success value, or when the awaited source is failed. + /// + /// + /// Thrown when the awaited source task is . + /// + /// + /// + /// var value = await resultTask.Else(42); + /// + /// + public Task Else(TSource alternative) + { + ArgumentNullException.ThrowIfNull(sourceTask); + + return ElseCore(sourceTask, source => Task.FromResult(source.Else(alternative))); + } + + /// + /// Returns the awaited successful value unchanged, or invokes a fallback factory when the awaited source is failed. + /// + /// The fallback factory to invoke when the awaited source is failed. + /// + /// A task producing the wrapped success value, or the value produced by when the awaited source is failed. + /// + /// + /// Thrown when the awaited source task or is . + /// + /// + /// + /// var value = await resultTask.Else(() => 42); + /// + /// + public Task Else(Func alternative) + { + ArgumentNullException.ThrowIfNull(sourceTask); + ArgumentNullException.ThrowIfNull(alternative); + + return ElseCore(sourceTask, source => Task.FromResult(source.Else(alternative))); + } + + /// + /// Returns the awaited successful value unchanged, or invokes a fallback factory with the awaited source failures when the awaited source is failed. + /// + /// The fallback factory to invoke with the awaited source failures. + /// + /// A task producing the wrapped success value, or the value produced by when the awaited source is failed. + /// + /// + /// Thrown when the awaited source task or is . + /// + /// + /// + /// var value = await resultTask.Else(failures => failures.Count); + /// + /// + public Task Else(Func, TSource> alternative) + { + ArgumentNullException.ThrowIfNull(sourceTask); + ArgumentNullException.ThrowIfNull(alternative); + + return ElseCore(sourceTask, source => Task.FromResult(source.Else(alternative))); + } + + /// + /// Returns the awaited successful value unchanged, or asynchronously invokes a fallback factory when the awaited source is failed. + /// + /// The asynchronous fallback factory to invoke when the awaited source is failed. + /// + /// A task producing the wrapped success value, or the value produced by when the awaited source is failed. + /// + /// + /// Thrown when the awaited source task or is , + /// or when returns . + /// + /// + /// + /// var value = await resultTask.ElseAsync(() => Task.FromResult(42)); + /// + /// + public Task ElseAsync(Func> alternativeAsync) + { + ArgumentNullException.ThrowIfNull(sourceTask); + ArgumentNullException.ThrowIfNull(alternativeAsync); + + return ElseCore(sourceTask, source => source.ElseAsync(alternativeAsync)); + } + + /// + /// Returns the awaited successful value unchanged, or asynchronously invokes a fallback factory with the awaited source failures when the awaited source is failed. + /// + /// The asynchronous fallback factory to invoke with the awaited source failures. + /// + /// A task producing the wrapped success value, or the value produced by when the awaited source is failed. + /// + /// + /// Thrown when the awaited source task or is , + /// or when returns . + /// + /// + /// + /// var value = await resultTask.ElseAsync(failures => Task.FromResult(failures.Count)); + /// + /// + public Task ElseAsync( + Func, Task> alternativeAsync + ) + { + ArgumentNullException.ThrowIfNull(sourceTask); + ArgumentNullException.ThrowIfNull(alternativeAsync); + + return ElseCore(sourceTask, source => source.ElseAsync(alternativeAsync)); + } + } + + private static async Task ElseAsyncCore(Func> alternativeAsync) + { + var resultTask = alternativeAsync(); + ArgumentNullException.ThrowIfNull(resultTask); + + return await resultTask.ConfigureAwait(false); + } + + private static async Task ElseAsyncCore( + IReadOnlyList failures, + Func, Task> alternativeAsync + ) + { + var resultTask = alternativeAsync(failures); + ArgumentNullException.ThrowIfNull(resultTask); + + return await resultTask.ConfigureAwait(false); + } + + private static async Task ElseCore( + Task> sourceTask, + Func, Task> terminate + ) + { + var source = await sourceTask.ConfigureAwait(false); + return await terminate(source).ConfigureAwait(false); + } +} diff --git a/tests/FailOr.Tests/ElseTestData.cs b/tests/FailOr.Tests/ElseTestData.cs new file mode 100644 index 0000000..5c1e739 --- /dev/null +++ b/tests/FailOr.Tests/ElseTestData.cs @@ -0,0 +1,460 @@ +namespace FailOr.Tests; + +public static class ElseTestData +{ + public static IEnumerable< + Func<(string Operation, Func, ElseInvocationCounter, Task> Invoke)> + > DirectSuccessCases() + { + yield return () => ("Else value", (source, _) => Task.FromResult(source.Else(99))); + yield return () => + ( + "Else deferred", + (source, counter) => Task.FromResult(source.Else(() => counter.Increment())) + ); + yield return () => + ( + "Else deferred failures-aware", + (source, counter) => + Task.FromResult(source.Else(failures => failures.Count + counter.Increment())) + ); + yield return () => + ( + "ElseAsync deferred", + (source, counter) => source.ElseAsync(() => Task.FromResult(counter.Increment())) + ); + yield return () => + ( + "ElseAsync deferred failures-aware", + (source, counter) => + source.ElseAsync(failures => + Task.FromResult(failures.Count + counter.Increment()) + ) + ); + } + + public static IEnumerable< + Func<( + string Operation, + Func, ElseInvocationCounter, Task> Invoke, + int ExpectedValue, + int ExpectedCalls + )> + > DirectFailureCases() + { + yield return () => ("Else value", (source, _) => Task.FromResult(source.Else(90)), 90, 0); + yield return () => + ( + "Else deferred", + (source, counter) => Task.FromResult(source.Else(() => 40 + counter.Increment())), + 41, + 1 + ); + yield return () => + ( + "Else deferred failures-aware", + (source, counter) => + Task.FromResult( + source.Else(failures => 50 + failures.Count + counter.Increment()) + ), + 52, + 1 + ); + yield return () => + ( + "ElseAsync deferred", + (source, counter) => + source.ElseAsync(() => Task.FromResult(60 + counter.Increment())), + 61, + 1 + ); + yield return () => + ( + "ElseAsync deferred failures-aware", + (source, counter) => + source.ElseAsync(failures => + Task.FromResult(70 + failures.Count + counter.Increment()) + ), + 72, + 1 + ); + } + + public static IEnumerable< + Func<(string Operation, Func, Task>> Capture)> + > DirectFailuresAwareCases() + { + yield return () => + ( + "Else deferred failures-aware", + source => + { + IReadOnlyList? observed = null; + + _ = source.Else(failures => + { + observed = failures; + return 1; + }); + + return Task.FromResult(observed!); + } + ); + + yield return () => + ( + "ElseAsync deferred failures-aware", + async source => + { + IReadOnlyList? observed = null; + + _ = await source.ElseAsync(failures => + { + observed = failures; + return Task.FromResult(1); + }); + + return observed!; + } + ); + } + + public static IEnumerable< + Func<(string Operation, Action Invoke, string ParameterName)> + > DirectNullSelectorCases() + { + yield return () => + ("Else deferred", () => FailOr.Success(1).Else((Func)null!), "alternative"); + yield return () => + ( + "Else deferred failures-aware", + () => FailOr.Success(1).Else((Func, int>)null!), + "alternative" + ); + yield return () => + ( + "ElseAsync deferred", + () => FailOr.Success(1).ElseAsync((Func>)null!), + "alternativeAsync" + ); + yield return () => + ( + "ElseAsync deferred failures-aware", + () => FailOr.Success(1).ElseAsync((Func, Task>)null!), + "alternativeAsync" + ); + } + + public static IEnumerable< + Func<(string Operation, Func Invoke, string ParameterName)> + > DirectNullTaskCases() + { + yield return () => + ( + "ElseAsync deferred", + () => FailOr.Fail(Failure.General("failed")).ElseAsync(() => null!), + "resultTask" + ); + yield return () => + ( + "ElseAsync deferred failures-aware", + () => FailOr.Fail(Failure.General("failed")).ElseAsync(_ => null!), + "resultTask" + ); + } + + public static IEnumerable< + Func<(string Operation, Func>, ElseInvocationCounter, Task> Invoke)> + > LiftedSuccessCases() + { + yield return () => ("Else value", (sourceTask, _) => sourceTask.Else(99)); + yield return () => + ("Else deferred", (sourceTask, counter) => sourceTask.Else(() => counter.Increment())); + yield return () => + ( + "Else deferred failures-aware", + (sourceTask, counter) => + sourceTask.Else(failures => failures.Count + counter.Increment()) + ); + yield return () => + ( + "ElseAsync deferred", + (sourceTask, counter) => + sourceTask.ElseAsync(() => Task.FromResult(counter.Increment())) + ); + yield return () => + ( + "ElseAsync deferred failures-aware", + (sourceTask, counter) => + sourceTask.ElseAsync(failures => + Task.FromResult(failures.Count + counter.Increment()) + ) + ); + } + + public static IEnumerable< + Func<( + string Operation, + Func>, ElseInvocationCounter, Task> Invoke, + int ExpectedValue, + int ExpectedCalls + )> + > LiftedFailureCases() + { + yield return () => ("Else value", (sourceTask, _) => sourceTask.Else(90), 90, 0); + yield return () => + ( + "Else deferred", + (sourceTask, counter) => sourceTask.Else(() => 40 + counter.Increment()), + 41, + 1 + ); + yield return () => + ( + "Else deferred failures-aware", + (sourceTask, counter) => + sourceTask.Else(failures => 50 + failures.Count + counter.Increment()), + 52, + 1 + ); + yield return () => + ( + "ElseAsync deferred", + (sourceTask, counter) => + sourceTask.ElseAsync(() => Task.FromResult(60 + counter.Increment())), + 61, + 1 + ); + yield return () => + ( + "ElseAsync deferred failures-aware", + (sourceTask, counter) => + sourceTask.ElseAsync(failures => + Task.FromResult(70 + failures.Count + counter.Increment()) + ), + 72, + 1 + ); + } + + public static IEnumerable< + Func<(string Operation, Func>, Task>> Capture)> + > LiftedFailuresAwareCases() + { + yield return () => + ( + "Else deferred failures-aware", + async sourceTask => + { + IReadOnlyList? observed = null; + + _ = await sourceTask.Else(failures => + { + observed = failures; + return 1; + }); + + return observed!; + } + ); + + yield return () => + ( + "ElseAsync deferred failures-aware", + async sourceTask => + { + IReadOnlyList? observed = null; + + _ = await sourceTask.ElseAsync(failures => + { + observed = failures; + return Task.FromResult(1); + }); + + return observed!; + } + ); + } + + public static IEnumerable< + Func<(string Operation, Action Invoke, string ParameterName)> + > LiftedNullSourceCases() + { + yield return () => ("Else value", () => ((Task>)null!).Else(1), "sourceTask"); + yield return () => + ("Else deferred", () => ((Task>)null!).Else(() => 1), "sourceTask"); + yield return () => + ( + "Else deferred failures-aware", + () => ((Task>)null!).Else(_ => 1), + "sourceTask" + ); + yield return () => + ( + "ElseAsync deferred", + () => ((Task>)null!).ElseAsync(() => Task.FromResult(1)), + "sourceTask" + ); + yield return () => + ( + "ElseAsync deferred failures-aware", + () => ((Task>)null!).ElseAsync(_ => Task.FromResult(1)), + "sourceTask" + ); + } + + public static IEnumerable< + Func<(string Operation, Action Invoke, string ParameterName)> + > LiftedNullSelectorCases() + { + yield return () => + ( + "Else deferred", + () => Task.FromResult(FailOr.Success(1)).Else((Func)null!), + "alternative" + ); + yield return () => + ( + "Else deferred failures-aware", + () => + Task.FromResult(FailOr.Success(1)) + .Else((Func, int>)null!), + "alternative" + ); + yield return () => + ( + "ElseAsync deferred", + () => Task.FromResult(FailOr.Success(1)).ElseAsync((Func>)null!), + "alternativeAsync" + ); + yield return () => + ( + "ElseAsync deferred failures-aware", + () => + Task.FromResult(FailOr.Success(1)) + .ElseAsync((Func, Task>)null!), + "alternativeAsync" + ); + } + + public static IEnumerable< + Func<(string Operation, Func Invoke, string ParameterName)> + > LiftedNullTaskCases() + { + yield return () => + ( + "ElseAsync deferred", + () => + Task.FromResult(FailOr.Fail(Failure.General("failed"))) + .ElseAsync(() => null!), + "resultTask" + ); + yield return () => + ( + "ElseAsync deferred failures-aware", + () => + Task.FromResult(FailOr.Fail(Failure.General("failed"))) + .ElseAsync(_ => null!), + "resultTask" + ); + } + + public static IEnumerable< + Func<( + string Operation, + FailOr Source, + Func, Task> Direct, + Func>, Task> Lifted, + int ExpectedValue + )> + > LiftedParityCases() + { + yield return () => + ( + "Else value success", + FailOr.Success(1), + source => Task.FromResult(source.Else(90)), + sourceTask => sourceTask.Else(90), + 1 + ); + yield return () => + ( + "Else deferred success", + FailOr.Success(1), + source => Task.FromResult(source.Else(() => 90)), + sourceTask => sourceTask.Else(() => 90), + 1 + ); + yield return () => + ( + "Else deferred failures-aware success", + FailOr.Success(1), + source => Task.FromResult(source.Else(_ => 90)), + sourceTask => sourceTask.Else(_ => 90), + 1 + ); + yield return () => + ( + "ElseAsync deferred success", + FailOr.Success(1), + source => source.ElseAsync(() => Task.FromResult(90)), + sourceTask => sourceTask.ElseAsync(() => Task.FromResult(90)), + 1 + ); + yield return () => + ( + "ElseAsync deferred failures-aware success", + FailOr.Success(1), + source => source.ElseAsync(_ => Task.FromResult(90)), + sourceTask => sourceTask.ElseAsync(_ => Task.FromResult(90)), + 1 + ); + yield return () => + ( + "Else value failure", + FailOr.Fail(Failure.General("failed")), + source => Task.FromResult(source.Else(90)), + sourceTask => sourceTask.Else(90), + 90 + ); + yield return () => + ( + "Else deferred failure", + FailOr.Fail(Failure.General("failed")), + source => Task.FromResult(source.Else(() => 90)), + sourceTask => sourceTask.Else(() => 90), + 90 + ); + yield return () => + ( + "Else deferred failures-aware failure", + FailOr.Fail(Failure.General("failed")), + source => Task.FromResult(source.Else(_ => 90)), + sourceTask => sourceTask.Else(_ => 90), + 90 + ); + yield return () => + ( + "ElseAsync deferred failure", + FailOr.Fail(Failure.General("failed")), + source => source.ElseAsync(() => Task.FromResult(90)), + sourceTask => sourceTask.ElseAsync(() => Task.FromResult(90)), + 90 + ); + yield return () => + ( + "ElseAsync deferred failures-aware failure", + FailOr.Fail(Failure.General("failed")), + source => source.ElseAsync(_ => Task.FromResult(90)), + sourceTask => sourceTask.ElseAsync(_ => Task.FromResult(90)), + 90 + ); + } +} + +public sealed class ElseInvocationCounter +{ + public int Calls { get; private set; } + + public int Increment() => ++Calls; +} diff --git a/tests/FailOr.Tests/FailOrElseTests.cs b/tests/FailOr.Tests/FailOrElseTests.cs new file mode 100644 index 0000000..0da096b --- /dev/null +++ b/tests/FailOr.Tests/FailOrElseTests.cs @@ -0,0 +1,85 @@ +using TUnit.Assertions; +using TUnit.Assertions.Extensions; +using TUnit.Core; + +namespace FailOr.Tests; + +public class FailOrElseTests +{ + [Test] + [MethodDataSource(typeof(ElseTestData), nameof(ElseTestData.DirectSuccessCases))] + public async Task Else_preserves_success_for_all_direct_overloads( + string operation, + Func, ElseInvocationCounter, Task> invoke + ) + { + var source = FailOr.Success(7); + var counter = new ElseInvocationCounter(); + + var result = await invoke(source, counter); + + using var _ = Assert.Multiple(); + await Assert.That(counter.Calls).IsEqualTo(0); + await Assert.That(result).IsEqualTo(7); + } + + [Test] + [MethodDataSource(typeof(ElseTestData), nameof(ElseTestData.DirectFailureCases))] + public async Task Else_uses_fallback_for_failed_direct_sources( + string operation, + Func, ElseInvocationCounter, Task> invoke, + int expectedValue, + int expectedCalls + ) + { + var source = FailOr.Fail(Failure.General($"{operation} failed")); + var counter = new ElseInvocationCounter(); + + var result = await invoke(source, counter); + + using var _ = Assert.Multiple(); + await Assert.That(counter.Calls).IsEqualTo(expectedCalls); + await Assert.That(result).IsEqualTo(expectedValue); + } + + [Test] + [MethodDataSource(typeof(ElseTestData), nameof(ElseTestData.DirectFailuresAwareCases))] + public async Task Else_failures_aware_direct_overloads_receive_the_original_failure_sequence( + string operation, + Func, Task>> capture + ) + { + var firstFailure = Failure.General($"{operation} first"); + var secondFailure = Failure.Validation("Value", $"{operation} second"); + var source = FailOr.Fail(firstFailure, secondFailure); + + var observed = await capture(source); + + using var _ = Assert.Multiple(); + await Assert.That(observed.Count).IsEqualTo(2); + await Assert.That(observed[0]).IsEqualTo(firstFailure); + await Assert.That(observed[1]).IsEqualTo(secondFailure); + } + + [Test] + [MethodDataSource(typeof(ElseTestData), nameof(ElseTestData.DirectNullSelectorCases))] + public async Task Null_selectors_throw_for_direct_Else_overloads( + string operation, + Action invoke, + string parameterName + ) + { + await Assert.That(invoke).Throws().WithParameterName(parameterName); + } + + [Test] + [MethodDataSource(typeof(ElseTestData), nameof(ElseTestData.DirectNullTaskCases))] + public async Task Null_tasks_throw_for_direct_async_Else_overloads( + string operation, + Func invoke, + string parameterName + ) + { + await Assert.That(invoke).Throws().WithParameterName(parameterName); + } +} diff --git a/tests/FailOr.Tests/TaskFailOrElseTests.cs b/tests/FailOr.Tests/TaskFailOrElseTests.cs new file mode 100644 index 0000000..0235bb6 --- /dev/null +++ b/tests/FailOr.Tests/TaskFailOrElseTests.cs @@ -0,0 +1,114 @@ +using TUnit.Assertions; +using TUnit.Assertions.Extensions; +using TUnit.Core; + +namespace FailOr.Tests; + +public class TaskFailOrElseTests +{ + [Test] + [MethodDataSource(typeof(ElseTestData), nameof(ElseTestData.LiftedSuccessCases))] + public async Task Else_preserves_success_for_all_lifted_overloads( + string operation, + Func>, ElseInvocationCounter, Task> invoke + ) + { + var source = FailOr.Success(7); + var counter = new ElseInvocationCounter(); + + var result = await invoke(Task.FromResult(source), counter); + + using var _ = Assert.Multiple(); + await Assert.That(counter.Calls).IsEqualTo(0); + await Assert.That(result).IsEqualTo(7); + } + + [Test] + [MethodDataSource(typeof(ElseTestData), nameof(ElseTestData.LiftedFailureCases))] + public async Task Else_uses_fallback_for_failed_lifted_sources( + string operation, + Func>, ElseInvocationCounter, Task> invoke, + int expectedValue, + int expectedCalls + ) + { + var sourceTask = Task.FromResult(FailOr.Fail(Failure.General($"{operation} failed"))); + var counter = new ElseInvocationCounter(); + + var result = await invoke(sourceTask, counter); + + using var _ = Assert.Multiple(); + await Assert.That(counter.Calls).IsEqualTo(expectedCalls); + await Assert.That(result).IsEqualTo(expectedValue); + } + + [Test] + [MethodDataSource(typeof(ElseTestData), nameof(ElseTestData.LiftedFailuresAwareCases))] + public async Task Else_failures_aware_lifted_overloads_receive_the_original_failure_sequence( + string operation, + Func>, Task>> capture + ) + { + var firstFailure = Failure.General($"{operation} first"); + var secondFailure = Failure.Validation("Value", $"{operation} second"); + var source = FailOr.Fail(firstFailure, secondFailure); + + var observed = await capture(Task.FromResult(source)); + + using var _ = Assert.Multiple(); + await Assert.That(observed.Count).IsEqualTo(2); + await Assert.That(observed[0]).IsEqualTo(firstFailure); + await Assert.That(observed[1]).IsEqualTo(secondFailure); + } + + [Test] + [MethodDataSource(typeof(ElseTestData), nameof(ElseTestData.LiftedNullSourceCases))] + public async Task Null_source_tasks_throw_for_lifted_Else_overloads( + string operation, + Action invoke, + string parameterName + ) + { + await Assert.That(invoke).Throws().WithParameterName(parameterName); + } + + [Test] + [MethodDataSource(typeof(ElseTestData), nameof(ElseTestData.LiftedNullSelectorCases))] + public async Task Null_selectors_throw_for_lifted_Else_overloads( + string operation, + Action invoke, + string parameterName + ) + { + await Assert.That(invoke).Throws().WithParameterName(parameterName); + } + + [Test] + [MethodDataSource(typeof(ElseTestData), nameof(ElseTestData.LiftedNullTaskCases))] + public async Task Null_tasks_throw_for_lifted_async_Else_overloads( + string operation, + Func invoke, + string parameterName + ) + { + await Assert.That(invoke).Throws().WithParameterName(parameterName); + } + + [Test] + [MethodDataSource(typeof(ElseTestData), nameof(ElseTestData.LiftedParityCases))] + public async Task Lifted_Else_overloads_match_direct_behavior( + string operation, + FailOr source, + Func, Task> direct, + Func>, Task> lifted, + int expectedValue + ) + { + var directResult = await direct(source); + var liftedResult = await lifted(Task.FromResult(source)); + + using var _ = Assert.Multiple(); + await Assert.That(liftedResult).IsEqualTo(directResult); + await Assert.That(liftedResult).IsEqualTo(expectedValue); + } +} From 0298eae6718c7781b0794b5131153b47db050e79 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Mar 2026 02:45:43 +0000 Subject: [PATCH 3/3] docs: clarify Else async exception conditions Co-authored-by: mark-pro <20671988+mark-pro@users.noreply.github.com> --- src/FailOr/FailOrT.Else.cs | 20 ++--- tests/FailOr.Tests/ElseTestData.cs | 125 ++++++++++++++++++----------- 2 files changed, 91 insertions(+), 54 deletions(-) diff --git a/src/FailOr/FailOrT.Else.cs b/src/FailOr/FailOrT.Else.cs index 6176bb5..12b15cc 100644 --- a/src/FailOr/FailOrT.Else.cs +++ b/src/FailOr/FailOrT.Else.cs @@ -68,7 +68,8 @@ public TSource Else(Func, TSource> alternative) /// A task producing the wrapped success value, or the value produced by when the source is failed. /// /// - /// Thrown when is or returns . + /// Thrown when is , + /// or when a failed source invokes and it returns . /// /// /// @@ -92,7 +93,8 @@ public Task ElseAsync(Func> alternativeAsync) /// A task producing the wrapped success value, or the value produced by when the source is failed. /// /// - /// Thrown when is or returns . + /// Thrown when is , + /// or when a failed source invokes and it returns . /// /// /// @@ -121,7 +123,7 @@ Func, Task> alternativeAsync /// A task producing the wrapped success value, or when the awaited source is failed. /// /// - /// Thrown when the awaited source task is . + /// Thrown when sourceTask is . /// /// /// @@ -143,7 +145,7 @@ public Task Else(TSource alternative) /// A task producing the wrapped success value, or the value produced by when the awaited source is failed. /// /// - /// Thrown when the awaited source task or is . + /// Thrown when sourceTask or is . /// /// /// @@ -166,7 +168,7 @@ public Task Else(Func alternative) /// A task producing the wrapped success value, or the value produced by when the awaited source is failed. /// /// - /// Thrown when the awaited source task or is . + /// Thrown when sourceTask or is . /// /// /// @@ -189,8 +191,8 @@ public Task Else(Func, TSource> alternative) /// A task producing the wrapped success value, or the value produced by when the awaited source is failed. /// /// - /// Thrown when the awaited source task or is , - /// or when returns . + /// Thrown when sourceTask or is , + /// or when the awaited source is failed, invokes , and it returns . /// /// /// @@ -213,8 +215,8 @@ public Task ElseAsync(Func> alternativeAsync) /// A task producing the wrapped success value, or the value produced by when the awaited source is failed. /// /// - /// Thrown when the awaited source task or is , - /// or when returns . + /// Thrown when sourceTask or is , + /// or when the awaited source is failed, invokes , and it returns . /// /// /// diff --git a/tests/FailOr.Tests/ElseTestData.cs b/tests/FailOr.Tests/ElseTestData.cs index 5c1e739..4fb3a41 100644 --- a/tests/FailOr.Tests/ElseTestData.cs +++ b/tests/FailOr.Tests/ElseTestData.cs @@ -2,11 +2,19 @@ namespace FailOr.Tests; public static class ElseTestData { + private const int SuccessAlternativeValue = 99; + private const int ImmediateFallbackValue = 90; + private const int DeferredFallbackBase = 40; + private const int FailuresAwareFallbackBase = 50; + private const int DeferredAsyncFallbackBase = 60; + private const int FailuresAwareAsyncFallbackBase = 70; + public static IEnumerable< Func<(string Operation, Func, ElseInvocationCounter, Task> Invoke)> > DirectSuccessCases() { - yield return () => ("Else value", (source, _) => Task.FromResult(source.Else(99))); + yield return () => + ("Else value", (source, _) => Task.FromResult(source.Else(SuccessAlternativeValue))); yield return () => ( "Else deferred", @@ -42,12 +50,19 @@ int ExpectedCalls )> > DirectFailureCases() { - yield return () => ("Else value", (source, _) => Task.FromResult(source.Else(90)), 90, 0); + yield return () => + ( + "Else value", + (source, _) => Task.FromResult(source.Else(ImmediateFallbackValue)), + ImmediateFallbackValue, + 0 + ); yield return () => ( "Else deferred", - (source, counter) => Task.FromResult(source.Else(() => 40 + counter.Increment())), - 41, + (source, counter) => + Task.FromResult(source.Else(() => DeferredFallbackBase + counter.Increment())), + DeferredFallbackBase + 1, 1 ); yield return () => @@ -55,17 +70,21 @@ int ExpectedCalls "Else deferred failures-aware", (source, counter) => Task.FromResult( - source.Else(failures => 50 + failures.Count + counter.Increment()) + source.Else(failures => + FailuresAwareFallbackBase + failures.Count + counter.Increment() + ) ), - 52, + FailuresAwareFallbackBase + 2, 1 ); yield return () => ( "ElseAsync deferred", (source, counter) => - source.ElseAsync(() => Task.FromResult(60 + counter.Increment())), - 61, + source.ElseAsync(() => + Task.FromResult(DeferredAsyncFallbackBase + counter.Increment()) + ), + DeferredAsyncFallbackBase + 1, 1 ); yield return () => @@ -73,9 +92,11 @@ int ExpectedCalls "ElseAsync deferred failures-aware", (source, counter) => source.ElseAsync(failures => - Task.FromResult(70 + failures.Count + counter.Increment()) + Task.FromResult( + FailuresAwareAsyncFallbackBase + failures.Count + counter.Increment() + ) ), - 72, + FailuresAwareAsyncFallbackBase + 2, 1 ); } @@ -167,7 +188,8 @@ public static IEnumerable< Func<(string Operation, Func>, ElseInvocationCounter, Task> Invoke)> > LiftedSuccessCases() { - yield return () => ("Else value", (sourceTask, _) => sourceTask.Else(99)); + yield return () => + ("Else value", (sourceTask, _) => sourceTask.Else(SuccessAlternativeValue)); yield return () => ("Else deferred", (sourceTask, counter) => sourceTask.Else(() => counter.Increment())); yield return () => @@ -201,28 +223,39 @@ int ExpectedCalls )> > LiftedFailureCases() { - yield return () => ("Else value", (sourceTask, _) => sourceTask.Else(90), 90, 0); + yield return () => + ( + "Else value", + (sourceTask, _) => sourceTask.Else(ImmediateFallbackValue), + ImmediateFallbackValue, + 0 + ); yield return () => ( "Else deferred", - (sourceTask, counter) => sourceTask.Else(() => 40 + counter.Increment()), - 41, + (sourceTask, counter) => + sourceTask.Else(() => DeferredFallbackBase + counter.Increment()), + DeferredFallbackBase + 1, 1 ); yield return () => ( "Else deferred failures-aware", (sourceTask, counter) => - sourceTask.Else(failures => 50 + failures.Count + counter.Increment()), - 52, + sourceTask.Else(failures => + FailuresAwareFallbackBase + failures.Count + counter.Increment() + ), + FailuresAwareFallbackBase + 2, 1 ); yield return () => ( "ElseAsync deferred", (sourceTask, counter) => - sourceTask.ElseAsync(() => Task.FromResult(60 + counter.Increment())), - 61, + sourceTask.ElseAsync(() => + Task.FromResult(DeferredAsyncFallbackBase + counter.Increment()) + ), + DeferredAsyncFallbackBase + 1, 1 ); yield return () => @@ -230,9 +263,11 @@ int ExpectedCalls "ElseAsync deferred failures-aware", (sourceTask, counter) => sourceTask.ElseAsync(failures => - Task.FromResult(70 + failures.Count + counter.Increment()) + Task.FromResult( + FailuresAwareAsyncFallbackBase + failures.Count + counter.Increment() + ) ), - 72, + FailuresAwareAsyncFallbackBase + 2, 1 ); } @@ -373,81 +408,81 @@ int ExpectedValue ( "Else value success", FailOr.Success(1), - source => Task.FromResult(source.Else(90)), - sourceTask => sourceTask.Else(90), + source => Task.FromResult(source.Else(ImmediateFallbackValue)), + sourceTask => sourceTask.Else(ImmediateFallbackValue), 1 ); yield return () => ( "Else deferred success", FailOr.Success(1), - source => Task.FromResult(source.Else(() => 90)), - sourceTask => sourceTask.Else(() => 90), + source => Task.FromResult(source.Else(() => ImmediateFallbackValue)), + sourceTask => sourceTask.Else(() => ImmediateFallbackValue), 1 ); yield return () => ( "Else deferred failures-aware success", FailOr.Success(1), - source => Task.FromResult(source.Else(_ => 90)), - sourceTask => sourceTask.Else(_ => 90), + source => Task.FromResult(source.Else(_ => ImmediateFallbackValue)), + sourceTask => sourceTask.Else(_ => ImmediateFallbackValue), 1 ); yield return () => ( "ElseAsync deferred success", FailOr.Success(1), - source => source.ElseAsync(() => Task.FromResult(90)), - sourceTask => sourceTask.ElseAsync(() => Task.FromResult(90)), + source => source.ElseAsync(() => Task.FromResult(ImmediateFallbackValue)), + sourceTask => sourceTask.ElseAsync(() => Task.FromResult(ImmediateFallbackValue)), 1 ); yield return () => ( "ElseAsync deferred failures-aware success", FailOr.Success(1), - source => source.ElseAsync(_ => Task.FromResult(90)), - sourceTask => sourceTask.ElseAsync(_ => Task.FromResult(90)), + source => source.ElseAsync(_ => Task.FromResult(ImmediateFallbackValue)), + sourceTask => sourceTask.ElseAsync(_ => Task.FromResult(ImmediateFallbackValue)), 1 ); yield return () => ( "Else value failure", FailOr.Fail(Failure.General("failed")), - source => Task.FromResult(source.Else(90)), - sourceTask => sourceTask.Else(90), - 90 + source => Task.FromResult(source.Else(ImmediateFallbackValue)), + sourceTask => sourceTask.Else(ImmediateFallbackValue), + ImmediateFallbackValue ); yield return () => ( "Else deferred failure", FailOr.Fail(Failure.General("failed")), - source => Task.FromResult(source.Else(() => 90)), - sourceTask => sourceTask.Else(() => 90), - 90 + source => Task.FromResult(source.Else(() => ImmediateFallbackValue)), + sourceTask => sourceTask.Else(() => ImmediateFallbackValue), + ImmediateFallbackValue ); yield return () => ( "Else deferred failures-aware failure", FailOr.Fail(Failure.General("failed")), - source => Task.FromResult(source.Else(_ => 90)), - sourceTask => sourceTask.Else(_ => 90), - 90 + source => Task.FromResult(source.Else(_ => ImmediateFallbackValue)), + sourceTask => sourceTask.Else(_ => ImmediateFallbackValue), + ImmediateFallbackValue ); yield return () => ( "ElseAsync deferred failure", FailOr.Fail(Failure.General("failed")), - source => source.ElseAsync(() => Task.FromResult(90)), - sourceTask => sourceTask.ElseAsync(() => Task.FromResult(90)), - 90 + source => source.ElseAsync(() => Task.FromResult(ImmediateFallbackValue)), + sourceTask => sourceTask.ElseAsync(() => Task.FromResult(ImmediateFallbackValue)), + ImmediateFallbackValue ); yield return () => ( "ElseAsync deferred failures-aware failure", FailOr.Fail(Failure.General("failed")), - source => source.ElseAsync(_ => Task.FromResult(90)), - sourceTask => sourceTask.ElseAsync(_ => Task.FromResult(90)), - 90 + source => source.ElseAsync(_ => Task.FromResult(ImmediateFallbackValue)), + sourceTask => sourceTask.ElseAsync(_ => Task.FromResult(ImmediateFallbackValue)), + ImmediateFallbackValue ); } }