From 1b8a7ef34992e2f76cf34f5124c5e78fed7c0988 Mon Sep 17 00:00:00 2001 From: Luca Fabbri Date: Tue, 16 Sep 2025 17:32:37 +0200 Subject: [PATCH 1/4] feat: adds error return to Else --- Maybe/MaybeExtensions.Else.cs | 63 +++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/Maybe/MaybeExtensions.Else.cs b/Maybe/MaybeExtensions.Else.cs index 6920e29..42366db 100644 --- a/Maybe/MaybeExtensions.Else.cs +++ b/Maybe/MaybeExtensions.Else.cs @@ -21,6 +21,16 @@ public static Maybe Else( : fallbackValue; } + public static Maybe Else( + this in Maybe maybe, + TError forwardedError) + where TError : BaseError, new() + { + return maybe.IsSuccess + ? maybe.ValueOrThrow() + : forwardedError; + } + /// /// Returns the success value or, if the Maybe is an error, the result of the provided fallback function. /// @@ -34,6 +44,19 @@ public static Maybe Else( : fallbackFunc(maybe.ErrorOrThrow()); } + /// + /// Returns the success value or, if the Maybe is an error, the provided fallback error. + /// + public static Maybe Else( + this in Maybe maybe, + Func fallbackErrorFunc) + where TError : BaseError, new() + { + return maybe.IsSuccess + ? maybe.ValueOrThrow() + : fallbackErrorFunc(maybe.ErrorOrThrow()); + } + #endregion #region Asynchronous Else on Task (with Sync Fallback) @@ -50,6 +73,16 @@ public static async Task> Else( return maybe.Else(fallbackValue); } + + public static async Task> Else( + this Task> maybeTask, + TError forwaredError) + where TError : BaseError, new() + { + var maybe = await maybeTask.ConfigureAwait(false); + return maybe.Else(forwaredError); + } + /// /// Asynchronously returns the success value or, if the Maybe is an error, the result of the provided fallback function. /// @@ -62,6 +95,16 @@ public static async Task> Else( return maybe.Else(fallbackFunc); } + + public static async Task> Else( + this Task> maybeTask, + Func fallbackErrorFunc) + where TError : BaseError, new() + { + var maybe = await maybeTask.ConfigureAwait(false); + return maybe.Else(fallbackErrorFunc); + } + #endregion #region Asynchronous ElseAsync (with Async Fallback) @@ -79,6 +122,16 @@ public static async Task> ElseAsync( : await fallbackAsync(maybe.ErrorOrThrow()); } + public static async Task> ElseAsync( + this Maybe maybe, + Func> forwardedErrorAsync) + where TError : BaseError, new() + { + return maybe.IsSuccess + ? maybe + : await forwardedErrorAsync(maybe.ErrorOrThrow()); + } + /// /// Asynchronously returns the success value or, if the Maybe is an error, asynchronously executes the fallback function and returns its result. /// @@ -91,5 +144,15 @@ public static async Task> ElseAsync( return await maybe.ElseAsync(fallbackAsync).ConfigureAwait(false); } + + public static async Task> ElseAsync( + this Task> maybeTask, + Func> fallbackErrorAsync) + where TError : BaseError, new() + { + var maybe = await maybeTask.ConfigureAwait(false); + return await maybe.ElseAsync(fallbackErrorAsync).ConfigureAwait(false); + } + #endregion } From a3d317f31f03af1b5adee97b856d6d62b277f389 Mon Sep 17 00:00:00 2001 From: Nataraja Date: Wed, 17 Sep 2025 01:28:23 +0200 Subject: [PATCH 2/4] Update MaybeExtensions.Else.cs --- Maybe/MaybeExtensions.Else.cs | 110 ++++++++++++++++++++++++++++------ 1 file changed, 92 insertions(+), 18 deletions(-) diff --git a/Maybe/MaybeExtensions.Else.cs b/Maybe/MaybeExtensions.Else.cs index 42366db..ded7bd9 100644 --- a/Maybe/MaybeExtensions.Else.cs +++ b/Maybe/MaybeExtensions.Else.cs @@ -1,4 +1,4 @@ -namespace Maybe; +namespace Maybe; /// /// Provides extension methods for exiting the Maybe context by providing a fallback value or function. @@ -9,51 +9,98 @@ public static partial class MaybeExtensions #region Synchronous Else (with Sync Fallback) /// - /// Returns the success value or, if the Maybe is an error, the provided fallback value. + /// Returns the original success as-is; otherwise returns the provided . /// + /// + /// Note: passing null as may be ambiguous when both TValue and TError + /// are reference types and other overloads exist. Prefer explicit casts or the delegate-based overloads. + /// + /// The Maybe to evaluate. + /// The value to return if is an error. + /// The success value type. + /// The error type. + /// + /// A successful Maybe with the original value, or a successful Maybe with . + /// public static Maybe Else( this in Maybe maybe, TValue fallbackValue) where TError : BaseError, new() { return maybe.IsSuccess - ? maybe.ValueOrThrow() + ? maybe : fallbackValue; } + /// + /// Returns the original success as-is; otherwise replaces the original error with the provided . + /// + /// + /// Useful to adapt low-level errors to higher-level domain errors. The original error is discarded. + /// + /// The Maybe to evaluate. + /// The error to return if is an error. + /// The success value type. + /// The error type. + /// + /// A successful Maybe with the original value, or an error Maybe with . + /// public static Maybe Else( this in Maybe maybe, TError forwardedError) where TError : BaseError, new() { return maybe.IsSuccess - ? maybe.ValueOrThrow() + ? maybe : forwardedError; } /// - /// Returns the success value or, if the Maybe is an error, the result of the provided fallback function. + /// Returns the original success as-is; otherwise returns the result of invoked with the original error. /// + /// + /// The function is not invoked if the Maybe is successful. + /// + /// The Maybe to evaluate. + /// The function that maps the original error to a fallback success value. + /// The success value type. + /// The error type. + /// Thrown if is null. public static Maybe Else( this in Maybe maybe, Func fallbackFunc) where TError : BaseError, new() { + if (fallbackFunc is null) throw new ArgumentNullException(nameof(fallbackFunc)); + return maybe.IsSuccess - ? maybe.ValueOrThrow() + ? maybe : fallbackFunc(maybe.ErrorOrThrow()); } /// - /// Returns the success value or, if the Maybe is an error, the provided fallback error. + /// Returns the original success as-is; otherwise transforms the original error via . /// + /// + /// The function is not invoked if the Maybe is successful. + /// + /// The Maybe to evaluate. + /// The function that maps the original error to a new error to return. + /// The success value type. + /// The error type. + /// + /// A successful Maybe with the original value, or an error Maybe with the transformed error. + /// + /// Thrown if is null. public static Maybe Else( this in Maybe maybe, Func fallbackErrorFunc) where TError : BaseError, new() { + if (fallbackErrorFunc is null) throw new ArgumentNullException(nameof(fallbackErrorFunc)); + return maybe.IsSuccess - ? maybe.ValueOrThrow() + ? maybe : fallbackErrorFunc(maybe.ErrorOrThrow()); } @@ -62,7 +109,7 @@ public static Maybe Else( #region Asynchronous Else on Task (with Sync Fallback) /// - /// Asynchronously returns the success value or, if the Maybe is an error, the provided fallback value. + /// Asynchronously returns the original success as-is; otherwise returns the provided . /// public static async Task> Else( this Task> maybeTask, @@ -73,34 +120,44 @@ public static async Task> Else( return maybe.Else(fallbackValue); } - + /// + /// Asynchronously returns the original success as-is; otherwise replaces the original error with . + /// public static async Task> Else( this Task> maybeTask, - TError forwaredError) + TError forwardedError) where TError : BaseError, new() { var maybe = await maybeTask.ConfigureAwait(false); - return maybe.Else(forwaredError); + return maybe.Else(forwardedError); } /// - /// Asynchronously returns the success value or, if the Maybe is an error, the result of the provided fallback function. + /// Asynchronously returns the original success as-is; otherwise returns the result of invoked with the original error. /// + /// Thrown if is null. public static async Task> Else( this Task> maybeTask, Func fallbackFunc) where TError : BaseError, new() { + if (fallbackFunc is null) throw new ArgumentNullException(nameof(fallbackFunc)); + var maybe = await maybeTask.ConfigureAwait(false); return maybe.Else(fallbackFunc); } - + /// + /// Asynchronously returns the original success as-is; otherwise transforms the original error via . + /// + /// Thrown if is null. public static async Task> Else( this Task> maybeTask, Func fallbackErrorFunc) where TError : BaseError, new() { + if (fallbackErrorFunc is null) throw new ArgumentNullException(nameof(fallbackErrorFunc)); + var maybe = await maybeTask.ConfigureAwait(false); return maybe.Else(fallbackErrorFunc); } @@ -110,49 +167,66 @@ public static async Task> Else( #region Asynchronous ElseAsync (with Async Fallback) /// - /// Returns the success value or, if the Maybe is an error, asynchronously executes the fallback function and returns its result. + /// Returns the original success as-is; otherwise asynchronously executes and returns its result. /// + /// Thrown if is null. public static async Task> ElseAsync( this Maybe maybe, Func> fallbackAsync) where TError : BaseError, new() { + if (fallbackAsync is null) throw new ArgumentNullException(nameof(fallbackAsync)); + return maybe.IsSuccess ? maybe : await fallbackAsync(maybe.ErrorOrThrow()); } + /// + /// Returns the original success as-is; otherwise asynchronously transforms the original error via . + /// + /// Thrown if is null. public static async Task> ElseAsync( this Maybe maybe, Func> forwardedErrorAsync) where TError : BaseError, new() { + if (forwardedErrorAsync is null) throw new ArgumentNullException(nameof(forwardedErrorAsync)); + return maybe.IsSuccess ? maybe : await forwardedErrorAsync(maybe.ErrorOrThrow()); } /// - /// Asynchronously returns the success value or, if the Maybe is an error, asynchronously executes the fallback function and returns its result. + /// Asynchronously returns the original success as-is; otherwise asynchronously executes and returns its result. /// + /// Thrown if is null. public static async Task> ElseAsync( this Task> maybeTask, Func> fallbackAsync) where TError : BaseError, new() { + if (fallbackAsync is null) throw new ArgumentNullException(nameof(fallbackAsync)); + var maybe = await maybeTask.ConfigureAwait(false); return await maybe.ElseAsync(fallbackAsync).ConfigureAwait(false); } - + /// + /// Asynchronously returns the original success as-is; otherwise asynchronously transforms the original error via . + /// + /// Thrown if is null. public static async Task> ElseAsync( this Task> maybeTask, Func> fallbackErrorAsync) where TError : BaseError, new() { + if (fallbackErrorAsync is null) throw new ArgumentNullException(nameof(fallbackErrorAsync)); + var maybe = await maybeTask.ConfigureAwait(false); return await maybe.ElseAsync(fallbackErrorAsync).ConfigureAwait(false); } #endregion -} +} \ No newline at end of file From 956faacb7b956825611ae6b0d4a1d98e04e2579f Mon Sep 17 00:00:00 2001 From: Nataraja Date: Wed, 17 Sep 2025 01:35:25 +0200 Subject: [PATCH 3/4] test: add coverage for Else/ElseAsync overloads and null-guard behavior --- Maybe.UnitTest/ElseErrorForwardingTests.cs | 210 +++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 Maybe.UnitTest/ElseErrorForwardingTests.cs diff --git a/Maybe.UnitTest/ElseErrorForwardingTests.cs b/Maybe.UnitTest/ElseErrorForwardingTests.cs new file mode 100644 index 0000000..daa45bb --- /dev/null +++ b/Maybe.UnitTest/ElseErrorForwardingTests.cs @@ -0,0 +1,210 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Maybe; +using Xunit; + +namespace Maybe.UnitTest; + +public class ElseErrorForwardingTests +{ + // Dummy error type used in tests. Must satisfy the "new()" constraint. + private sealed class DummyError : BaseError + { + public string? Info { get; } + public DummyError() { } + public DummyError(string? info) => Info = info; + } + + [Fact] + public void Else_WithForwardedError_ReturnsSuccess_WhenSuccess() + { + var success = Maybe.Success(42); + + var result = success.Else(new DummyError("fwd")); + + result.IsSuccess.Should().BeTrue(); + result.ValueOrThrow().Should().Be(42); + } + + [Fact] + public void Else_WithForwardedError_ReplacesError_WhenError() + { + var original = Maybe.Error(new DummyError("orig")); + + var fwd = new DummyError("fwd"); + var result = original.Else(fwd); + + result.IsError.Should().BeTrue(); + result.ErrorOrThrow().Should().BeSameAs(fwd); + } + + [Fact] + public void Else_WithValueFallback_ReturnsFallback_WhenError() + { + var original = Maybe.Error(new DummyError("orig")); + + var result = original.Else("fallback"); + + result.IsSuccess.Should().BeTrue(); + result.ValueOrThrow().Should().Be("fallback"); + } + + [Fact] + public void Else_WithValueFunc_UsesFunc_WhenError_And_NotInvoked_WhenSuccess() + { + var invoked = false; + Func func = e => + { + invoked = true; + return $"wrapped:" + e.Info; + }; + + var success = Maybe.Success("ok"); + success.Else(func).IsSuccess.Should().BeTrue(); + invoked.Should().BeFalse(); + + invoked = false; + var error = Maybe.Error(new DummyError("orig")); + var result = error.Else(func); + invoked.Should().BeTrue(); + result.IsSuccess.Should().BeTrue(); + result.ValueOrThrow().Should().Be("wrapped:orig"); + } + + [Fact] + public void Else_WithErrorFunc_TransformsError_WhenError_And_NotInvoked_WhenSuccess() + { + var invoked = false; + Func func = e => + { + invoked = true; + return new DummyError($"x:" + e.Info); + }; + + var success = Maybe.Success(1); + var s = success.Else(func); + s.IsSuccess.Should().BeTrue(); + invoked.Should().BeFalse(); + + invoked = false; + var err = Maybe.Error(new DummyError("orig")); + var r = err.Else(func); + invoked.Should().BeTrue(); + r.IsError.Should().BeTrue(); + r.ErrorOrThrow().Info.Should().Be("x:orig"); + } + + [Fact] + public void Else_ValueFunc_Null_Throws() + { + var err = Maybe.Error(new DummyError("e")); + Action act = () => err.Else((Func)null!); + act.Should().Throw().WithParameterName("fallbackFunc"); + } + + [Fact] + public void Else_ErrorFunc_Null_Throws() + { + var err = Maybe.Error(new DummyError("e")); + Action act = () => err.Else((Func)null!); + act.Should().Throw().WithParameterName("fallbackErrorFunc"); + } + + [Fact] + public async Task Else_Task_WithForwardedError_ReplacesError_WhenError() + { + Task> task = Task.FromResult(Maybe.Error(new DummyError("orig"))); + + var forwarded = new DummyError("fwd"); + var result = await task.Else(forwarded); + + result.IsError.Should().BeTrue(); + result.ErrorOrThrow().Should().BeSameAs(forwarded); + } + + [Fact] + public async Task Else_Task_WithValueFunc_Works_WhenError_And_NotInvoked_WhenSuccess() + { + var invoked = false; + Func func = e => + { + invoked = true; + return "val"; + }; + + var sTask = Task.FromResult(Maybe.Success("ok")); + (await sTask.Else(func)).IsSuccess.Should().BeTrue(); + invoked.Should().BeFalse(); + + invoked = false; + var eTask = Task.FromResult(Maybe.Error(new DummyError("e"))); + var r = await eTask.Else(func); + invoked.Should().BeTrue(); + r.IsSuccess.Should().BeTrue(); + r.ValueOrThrow().Should().Be("val"); + } + + [Fact] + public async Task ElseAsync_WithAsyncValueFunc_UsesTransformedValue_WhenError() + { + var maybe = Maybe.Error(new DummyError("orig")); + + var result = await maybe.ElseAsync(async e => + { + await Task.Delay(1); + return 99; + }); + + result.IsSuccess.Should().BeTrue(); + result.ValueOrThrow().Should().Be(99); + } + + [Fact] + public async Task ElseAsync_WithAsyncErrorFunc_UsesTransformedError_WhenError() + { + var maybe = Maybe.Error(new DummyError("orig")); + + var result = await maybe.ElseAsync(async e => + { + await Task.Delay(1); + return new DummyError($"async:" + e.Info); + }); + + result.IsError.Should().BeTrue(); + result.ErrorOrThrow().Info.Should().Be("async:orig"); + } + + [Fact] + public async Task ElseAsync_Task_WithAsyncErrorFunc_UsesTransformedError_WhenError() + { + var task = Task.FromResult(Maybe.Error(new DummyError("orig"))); + + var result = await task.ElseAsync(async e => + { + await Task.Delay(1); + return new DummyError($"async:" + e.Info); + }); + + result.IsError.Should().BeTrue(); + result.ErrorOrThrow().Info.Should().Be("async:orig"); + } + + [Fact] + public async Task ElseAsync_ValueFunc_Null_Throws() + { + var err = Maybe.Error(new DummyError("e")); + Func> f = null!; + var act = async () => await err.ElseAsync(f); + await act.Should().ThrowAsync().WithParameterName("fallbackAsync"); + } + + [Fact] + public async Task ElseAsync_ErrorFunc_Null_Throws() + { + var err = Maybe.Error(new DummyError("e")); + Func> f = null!; + var act = async () => await err.ElseAsync(f); + await act.Should().ThrowAsync().WithParameterName("forwardedErrorAsync"); + } +} \ No newline at end of file From bac8d13ac5491068a6775c98cfd181b207a5b815 Mon Sep 17 00:00:00 2001 From: Luca Fabbri Date: Wed, 17 Sep 2025 09:27:02 +0200 Subject: [PATCH 4/4] fix: corrects units tests --- Maybe.UnitTest/ElseErrorForwardingTests.cs | 36 +++++++++++----------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Maybe.UnitTest/ElseErrorForwardingTests.cs b/Maybe.UnitTest/ElseErrorForwardingTests.cs index daa45bb..04f0a6c 100644 --- a/Maybe.UnitTest/ElseErrorForwardingTests.cs +++ b/Maybe.UnitTest/ElseErrorForwardingTests.cs @@ -9,7 +9,7 @@ namespace Maybe.UnitTest; public class ElseErrorForwardingTests { // Dummy error type used in tests. Must satisfy the "new()" constraint. - private sealed class DummyError : BaseError + private sealed class DummyError : Error { public string? Info { get; } public DummyError() { } @@ -19,7 +19,7 @@ public DummyError() { } [Fact] public void Else_WithForwardedError_ReturnsSuccess_WhenSuccess() { - var success = Maybe.Success(42); + var success = 42.MightBe(); var result = success.Else(new DummyError("fwd")); @@ -30,7 +30,7 @@ public void Else_WithForwardedError_ReturnsSuccess_WhenSuccess() [Fact] public void Else_WithForwardedError_ReplacesError_WhenError() { - var original = Maybe.Error(new DummyError("orig")); + var original = new DummyError("orig").MightBe(); var fwd = new DummyError("fwd"); var result = original.Else(fwd); @@ -42,7 +42,7 @@ public void Else_WithForwardedError_ReplacesError_WhenError() [Fact] public void Else_WithValueFallback_ReturnsFallback_WhenError() { - var original = Maybe.Error(new DummyError("orig")); + var original = new DummyError("orig").MightBe(); var result = original.Else("fallback"); @@ -60,12 +60,12 @@ public void Else_WithValueFunc_UsesFunc_WhenError_And_NotInvoked_WhenSuccess() return $"wrapped:" + e.Info; }; - var success = Maybe.Success("ok"); + var success = "ok".MightBe(); success.Else(func).IsSuccess.Should().BeTrue(); invoked.Should().BeFalse(); invoked = false; - var error = Maybe.Error(new DummyError("orig")); + var error = new DummyError("orig").MightBe(); var result = error.Else(func); invoked.Should().BeTrue(); result.IsSuccess.Should().BeTrue(); @@ -82,13 +82,13 @@ public void Else_WithErrorFunc_TransformsError_WhenError_And_NotInvoked_WhenSucc return new DummyError($"x:" + e.Info); }; - var success = Maybe.Success(1); + var success = 1.MightBe(); var s = success.Else(func); s.IsSuccess.Should().BeTrue(); invoked.Should().BeFalse(); invoked = false; - var err = Maybe.Error(new DummyError("orig")); + var err = new DummyError("orig").MightBe(); var r = err.Else(func); invoked.Should().BeTrue(); r.IsError.Should().BeTrue(); @@ -98,7 +98,7 @@ public void Else_WithErrorFunc_TransformsError_WhenError_And_NotInvoked_WhenSucc [Fact] public void Else_ValueFunc_Null_Throws() { - var err = Maybe.Error(new DummyError("e")); + var err = new DummyError("e").MightBe(); Action act = () => err.Else((Func)null!); act.Should().Throw().WithParameterName("fallbackFunc"); } @@ -106,7 +106,7 @@ public void Else_ValueFunc_Null_Throws() [Fact] public void Else_ErrorFunc_Null_Throws() { - var err = Maybe.Error(new DummyError("e")); + var err = new DummyError("e").MightBe(); Action act = () => err.Else((Func)null!); act.Should().Throw().WithParameterName("fallbackErrorFunc"); } @@ -114,7 +114,7 @@ public void Else_ErrorFunc_Null_Throws() [Fact] public async Task Else_Task_WithForwardedError_ReplacesError_WhenError() { - Task> task = Task.FromResult(Maybe.Error(new DummyError("orig"))); + Task> task = Task.FromResult(new DummyError("orig").MightBe()); var forwarded = new DummyError("fwd"); var result = await task.Else(forwarded); @@ -133,12 +133,12 @@ public async Task Else_Task_WithValueFunc_Works_WhenError_And_NotInvoked_WhenSuc return "val"; }; - var sTask = Task.FromResult(Maybe.Success("ok")); + var sTask = Task.FromResult("ok".MightBe()); (await sTask.Else(func)).IsSuccess.Should().BeTrue(); invoked.Should().BeFalse(); invoked = false; - var eTask = Task.FromResult(Maybe.Error(new DummyError("e"))); + var eTask = Task.FromResult(new DummyError("e").MightBe()); var r = await eTask.Else(func); invoked.Should().BeTrue(); r.IsSuccess.Should().BeTrue(); @@ -148,7 +148,7 @@ public async Task Else_Task_WithValueFunc_Works_WhenError_And_NotInvoked_WhenSuc [Fact] public async Task ElseAsync_WithAsyncValueFunc_UsesTransformedValue_WhenError() { - var maybe = Maybe.Error(new DummyError("orig")); + var maybe = new DummyError("orig").MightBe(); var result = await maybe.ElseAsync(async e => { @@ -163,7 +163,7 @@ public async Task ElseAsync_WithAsyncValueFunc_UsesTransformedValue_WhenError() [Fact] public async Task ElseAsync_WithAsyncErrorFunc_UsesTransformedError_WhenError() { - var maybe = Maybe.Error(new DummyError("orig")); + var maybe = new DummyError("orig").MightBe(); var result = await maybe.ElseAsync(async e => { @@ -178,7 +178,7 @@ public async Task ElseAsync_WithAsyncErrorFunc_UsesTransformedError_WhenError() [Fact] public async Task ElseAsync_Task_WithAsyncErrorFunc_UsesTransformedError_WhenError() { - var task = Task.FromResult(Maybe.Error(new DummyError("orig"))); + var task = Task.FromResult(new DummyError("orig").MightBe()); var result = await task.ElseAsync(async e => { @@ -193,7 +193,7 @@ public async Task ElseAsync_Task_WithAsyncErrorFunc_UsesTransformedError_WhenErr [Fact] public async Task ElseAsync_ValueFunc_Null_Throws() { - var err = Maybe.Error(new DummyError("e")); + var err = new DummyError("e").MightBe(); Func> f = null!; var act = async () => await err.ElseAsync(f); await act.Should().ThrowAsync().WithParameterName("fallbackAsync"); @@ -202,7 +202,7 @@ public async Task ElseAsync_ValueFunc_Null_Throws() [Fact] public async Task ElseAsync_ErrorFunc_Null_Throws() { - var err = Maybe.Error(new DummyError("e")); + var err = new DummyError("e").MightBe(); Func> f = null!; var act = async () => await err.ElseAsync(f); await act.Should().ThrowAsync().WithParameterName("forwardedErrorAsync");