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
);
}
}