Skip to content

Add terminal Else overloads for FailOr<T> and Task<FailOr<T>>#8

Merged
mark-pro merged 3 commits intomainfrom
copilot/add-else-overloads-to-failor
Mar 9, 2026
Merged

Add terminal Else overloads for FailOr<T> and Task<FailOr<T>>#8
mark-pro merged 3 commits intomainfrom
copilot/add-else-overloads-to-failor

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 9, 2026

This adds a dedicated terminal Else API that collapses FailOr<TSource> into TSource: return the success value as-is, or produce a fallback value on failure. It complements IfFailThen by covering the common “identity on success, fallback on failure” path without staying inside FailOr<T>.

  • New terminal extension surface

    • Added /src/FailOr/FailOrT.Else.cs
    • Introduced direct overloads for FailOr<TSource>:
      • Else(TSource alternative)
      • Else(Func<TSource> alternative)
      • Else(Func<IReadOnlyList<Failures>, TSource> alternative)
      • ElseAsync(Func<Task<TSource>> alternativeAsync)
      • ElseAsync(Func<IReadOnlyList<Failures>, Task<TSource>> alternativeAsync)
    • Introduced matching lifted overloads for Task<FailOr<TSource>>
  • Behavior and guard semantics

    • Success path returns the wrapped value unchanged
    • Failure path returns the immediate alternative or invokes the fallback delegate
    • Deferred overloads stay lazy on success
    • Failure-aware overloads receive the original Failures sequence in order
    • Async overloads validate null delegates and null returned tasks using the same guard-clause conventions as existing extension APIs
  • Dedicated coverage

    • Added focused tests in:
      • /tests/FailOr.Tests/FailOrElseTests.cs
      • /tests/FailOr.Tests/TaskFailOrElseTests.cs
      • /tests/FailOr.Tests/ElseTestData.cs
    • Covers direct and lifted parity, success/failure behavior, laziness, failure-sequence passthrough, and validation cases

Example:

var value = result.Else(42);
var deferred = result.Else(() => 42);
var fromFailures = result.Else(failures => failures.Count);

var asyncValue = await result.ElseAsync(() => GetFallbackAsync());
var liftedValue = await resultTask.Else(42);
var liftedAsyncValue = await resultTask.ElseAsync(() => GetFallbackAsync());
Original prompt

This section details on the original issue you should resolve

<issue_title>Add terminal Else overloads for FailOr and Task<FailOr></issue_title>
<issue_description>## Summary

Add an Else API surface that terminates FailOr<TSource> into TSource by returning the successful value when present or an alternative value when the result is failed.

This should complement the existing IfFailThen recovery APIs:

  • IfFailThen keeps the caller inside FailOr<TSource>
  • Else should terminate to TSource

It should also provide a lighter-weight alternative to Match for the common "identity on success, fallback on failure" case.

Scope

Implementation should be isolated to a new extension file and a complete dedicated test suite.

Required production file:

  • src/FailOr/FailOrT.Else.cs

Acceptable test files:

  • tests/FailOr.Tests/FailOrElseTests.cs
  • tests/FailOr.Tests/TaskFailOrElseTests.cs
  • tests/FailOr.Tests/ElseTestData.cs

No other existing source files should need to be modified for this change.

Proposed API

Add a new extension class in src/FailOr/FailOrT.Else.cs, following the existing extension-block pattern used by FailOrT.Then.cs and FailOrT.Match.cs.

Direct overloads for FailOr<TSource>

public TSource Else(TSource alternative)
public TSource Else(Func<TSource> alternative)
public TSource Else(Func<IReadOnlyList<Failures>, TSource> alternative)
public Task<TSource> ElseAsync(Func<Task<TSource>> alternativeAsync)
public Task<TSource> ElseAsync(Func<IReadOnlyList<Failures>, Task<TSource>> alternativeAsync)

Lifted overloads for Task<FailOr<TSource>>

public Task<TSource> Else(TSource alternative)
public Task<TSource> Else(Func<TSource> alternative)
public Task<TSource> Else(Func<IReadOnlyList<Failures>, TSource> alternative)
public Task<TSource> ElseAsync(Func<Task<TSource>> alternativeAsync)
public Task<TSource> ElseAsync(Func<IReadOnlyList<Failures>, Task<TSource>> alternativeAsync)

Behavioral Requirements

  • When the source is successful, Else must return the wrapped success value unchanged.
  • When the source is failed, Else must return the immediate alternative or invoke the fallback delegate and return its result.
  • Deferred overloads must be lazy and must not invoke the fallback when the source is successful.
  • Failure-aware overloads must receive the original Failures sequence in the same order exposed by source.Failures.
  • Async overloads must only invoke the async fallback when the source is failed.
  • Lifted Task<FailOr<TSource>> overloads must match the behavior of the direct overloads after awaiting the source task.
  • Delegate and task validation should follow the existing conventions in FailOrT.Then.cs and FailOrT.Match.cs.

Validation Requirements

Match the repository's current guard-clause behavior and parameter naming conventions.

Expected validation coverage:

  • ArgumentNullException when a fallback delegate is null
  • ArgumentNullException when a lifted sourceTask is null
  • ArgumentNullException when an async fallback delegate returns null

Documentation Requirements

Each new public function should include XML documentation comments with:

  • summary
  • parameters
  • return value
  • documented exceptions
  • a succinct example

Test Requirements

Add a complete accompanying test suite that covers all direct and lifted overloads.

Minimum coverage:

  • success path returns the wrapped value for every overload
  • failure path returns the provided fallback value for every overload
  • deferred overloads are not invoked on success
  • deferred overloads are invoked exactly once on failure
  • failure-aware overloads receive the original failure collection unchanged
  • null delegate cases throw with the expected parameter name
  • null lifted source-task cases throw with the expected parameter name
  • async overloads that return a null task throw with the expected parameter name
  • lifted overloads match the direct overload behavior for both success and failure

Example Usage

var value = result.Else(42);
var value = result.Else(() => 42);
var value = result.Else(failures => failures.Count);

var asyncValue = await result.ElseAsync(() => GetFallbackAsync());
var asyncValue = await result.ElseAsync(failures => GetFallbackAsync(failures));

var liftedValue = await resultTask.Else(42);
var liftedAsyncValue = await resultTask.ElseAsync(() => GetFallbackAsync());

Acceptance Criteria

  • Else exists as a dedicated extension surface in src/FailOr/FailOrT.Else.cs
  • The API includes immediate, deferred, failure-aware, sync, and async overloads for both direct and lifted sources
  • The new API terminates to TSource, not FailOr<TSource>
  • The implementation follows the repository's existing extension and guard-clause patterns
  • The accompanying tests fully cover the new API surface
  • No existing production files need to be changed outside of adding the new exte...

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 2 commits March 9, 2026 02:41
Co-authored-by: mark-pro <20671988+mark-pro@users.noreply.github.com>
Co-authored-by: mark-pro <20671988+mark-pro@users.noreply.github.com>
Copilot AI changed the title [WIP] Add terminal Else overloads for FailOr<T> and Task<FailOr<T>> Add terminal Else overloads for FailOr<T> and Task<FailOr<T>> Mar 9, 2026
@mark-pro mark-pro marked this pull request as ready for review March 9, 2026 18:03
@mark-pro mark-pro merged commit 3326614 into main Mar 9, 2026
1 check passed
@mark-pro mark-pro deleted the copilot/add-else-overloads-to-failor branch March 10, 2026 00:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add terminal Else overloads for FailOr<T> and Task<FailOr<T>>

2 participants