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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ It centers on three public entry points:
The library also includes convenience APIs for common result workflows:

- `FailOr.Success(...)` and `FailOr.Fail(...)` for construction
- `Then(...)`, `ThenAsync(...)`, `ThenDo(...)`, and `ThenDoAsync(...)` for chaining and success-side effects
- `Then(...)`, `ThenAsync(...)`, `ThenEnsure(...)`, `ThenEnsureAsync(...)`, `ThenDo(...)`, and `ThenDoAsync(...)` for chaining and success-side effects
- `Match(...)` and `MatchFirst(...)` for branching
- `Zip(...)` for aggregating multiple results
- `Combine(...)` for choosing a preferred success with fallback
Expand Down Expand Up @@ -146,7 +146,7 @@ if (result.IsFailure)
}
```

You can also translate the exception into a custom repo-native result:
You can also translate the exception into a custom repository-native result:

```csharp
using FailOr;
Expand All @@ -157,6 +157,38 @@ var result = FailOr.Success("42x")
exception => Failure.General($"Mapping failed: {exception.Message}"));
```

### Validate success values with `ThenEnsure`

Use `ThenEnsure` when the next step should validate the current success and keep that original value flowing when validation succeeds.

```csharp
using FailOr;

var result = FailOr.Success(10)
.ThenEnsure(value =>
value >= 0
? FailOr.Success(true)
: FailOr.Fail<bool>(Failure.General("Value must be non-negative.")))
.Then(value => value + 5);

var finalValue = result.UnsafeUnwrap(); // 15
```

Async validation helpers are also available:

```csharp
using FailOr;

var result = await FailOr.Success(10)
.ThenEnsureAsync(async value =>
{
await Task.Delay(10);
return value % 2 == 0
? FailOr.Success(true)
: FailOr.Fail<bool>(Failure.General("Value must be even."));
});
```

### Run success-side effects with `ThenDo`

Use `ThenDo` when you want to observe a success without changing the flowing result.
Expand Down
41 changes: 41 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,25 @@ var result = FailOr.Success("42")
.Then(ParseNumber);
```

### Validate with another `FailOr` and preserve the success

```csharp
public FailOr<TSource> ThenEnsure<TResult>(Func<TSource, FailOr<TResult>> ensure)
```

Intent:
Run a validation step that can fail while keeping the original success value unchanged when validation succeeds.

Example:

```csharp
var result = FailOr.Success(10)
.ThenEnsure(value =>
value >= 0
? FailOr.Success(true)
: FailOr.Fail<bool>(Failure.General("Value must be non-negative.")));
```

### Map asynchronously

```csharp
Expand Down Expand Up @@ -354,6 +373,28 @@ var result = await FailOr.Success("42")
.ThenAsync(ParseNumberAsync);
```

### Validate asynchronously and preserve the success

```csharp
public Task<FailOr<TSource>> ThenEnsureAsync<TResult>(Func<TSource, Task<FailOr<TResult>>> ensureAsync)
```

Intent:
Run an asynchronous validation step that can fail while keeping the original success value unchanged when validation succeeds.

Example:

```csharp
var result = await FailOr.Success(10)
.ThenEnsureAsync(async value =>
{
await Task.Delay(10);
return value % 2 == 0
? FailOr.Success(true)
: FailOr.Fail<bool>(Failure.General("Value must be even."));
});
```

### Run a side effect and preserve the success

```csharp
Expand Down
122 changes: 122 additions & 0 deletions src/FailOr/FailOrT.Then.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,29 @@ public FailOr<TResult> Then<TResult>(Func<TSource, FailOr<TResult>> bind)
return source.IsFailure ? Fail<TSource, TResult>(source) : bind(source.UnsafeUnwrap());
}

/// <summary>
/// Validates a successful value with another <see cref="FailOr{T}"/> check while preserving the original success.
/// </summary>
/// <param name="ensure">The validation function to apply when the source is successful.</param>
/// <returns>
/// The original success unchanged when the validation succeeds, the validation failures when it fails,
/// or the original failures when the source is failed.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="ensure"/> is <see langword="null"/>.
/// </exception>
/// <example>
/// <code>
/// var next = result.ThenEnsure(x => x > 0 ? FailOr.Success(true) : FailOr.Fail&lt;bool&gt;(Failure.General("Must be positive")));
/// </code>
/// </example>
public FailOr<TSource> ThenEnsure<TResult>(Func<TSource, FailOr<TResult>> ensure)
{
ArgumentNullException.ThrowIfNull(ensure);

return source.IsFailure ? source : ThenEnsureCore(source.UnsafeUnwrap(), ensure);
}

/// <summary>
/// Asynchronously maps a successful value to a new successful result.
/// </summary>
Expand Down Expand Up @@ -103,6 +126,33 @@ Func<TSource, Task<FailOr<TResult>>> bindAsync
: ThenBindAsync(source.UnsafeUnwrap(), bindAsync);
}

/// <summary>
/// Asynchronously validates a successful value with another <see cref="FailOr{T}"/> check while preserving the original success.
/// </summary>
/// <param name="ensureAsync">The asynchronous validation function to apply when the source is successful.</param>
/// <returns>
/// A task producing the original success unchanged when the validation succeeds, the validation failures when it fails,
/// or the original failures when the source is failed.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="ensureAsync"/> is <see langword="null"/> or returns <see langword="null"/>.
/// </exception>
/// <example>
/// <code>
/// var next = await result.ThenEnsureAsync(x => Task.FromResult(x > 0 ? FailOr.Success(true) : FailOr.Fail&lt;bool&gt;(Failure.General("Must be positive"))));
/// </code>
/// </example>
public Task<FailOr<TSource>> ThenEnsureAsync<TResult>(
Func<TSource, Task<FailOr<TResult>>> ensureAsync
)
{
ArgumentNullException.ThrowIfNull(ensureAsync);

return source.IsFailure
? Task.FromResult(source)
: ThenEnsureAsyncCore(source.UnsafeUnwrap(), ensureAsync);
}

/// <summary>
/// Runs a side effect for a successful value while preserving the original result.
/// </summary>
Expand Down Expand Up @@ -315,6 +365,30 @@ public Task<FailOr<TResult>> Then<TResult>(Func<TSource, FailOr<TResult>> bind)
return ThenCore(sourceTask, source => Task.FromResult(source.Then(bind)));
}

/// <summary>
/// Validates the successful value of a task-wrapped result with another <see cref="FailOr{T}"/> check while preserving the original success.
/// </summary>
/// <param name="ensure">The validation function to apply when the awaited source is successful.</param>
/// <returns>
/// A task producing the original success unchanged when the validation succeeds, the validation failures when it fails,
/// or the original failures when the awaited source is failed.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Thrown when the awaited source task or <paramref name="ensure"/> is <see langword="null"/>.
/// </exception>
/// <example>
/// <code>
/// var next = await resultTask.ThenEnsure(x => x > 0 ? FailOr.Success(true) : FailOr.Fail&lt;bool&gt;(Failure.General("Must be positive")));
/// </code>
/// </example>
public Task<FailOr<TSource>> ThenEnsure<TResult>(Func<TSource, FailOr<TResult>> ensure)
{
ArgumentNullException.ThrowIfNull(sourceTask);
ArgumentNullException.ThrowIfNull(ensure);

return ThenCore(sourceTask, source => Task.FromResult(source.ThenEnsure(ensure)));
}

/// <summary>
/// Asynchronously maps the successful value of a task-wrapped result to a new successful result.
/// </summary>
Expand Down Expand Up @@ -363,6 +437,33 @@ Func<TSource, Task<FailOr<TResult>>> bindAsync
return ThenCore(sourceTask, source => source.ThenAsync(bindAsync));
}

/// <summary>
/// Asynchronously validates the successful value of a task-wrapped result with another <see cref="FailOr{T}"/> check while preserving the original success.
/// </summary>
/// <param name="ensureAsync">The asynchronous validation function to apply when the awaited source is successful.</param>
/// <returns>
/// A task producing the original success unchanged when the validation succeeds, the validation failures when it fails,
/// or the original failures when the awaited source is failed.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Thrown when the awaited source task or <paramref name="ensureAsync"/> is <see langword="null"/>,
/// or when <paramref name="ensureAsync"/> returns <see langword="null"/>.
/// </exception>
/// <example>
/// <code>
/// var next = await resultTask.ThenEnsureAsync(x => Task.FromResult(x > 0 ? FailOr.Success(true) : FailOr.Fail&lt;bool&gt;(Failure.General("Must be positive"))));
/// </code>
/// </example>
public Task<FailOr<TSource>> ThenEnsureAsync<TResult>(
Func<TSource, Task<FailOr<TResult>>> ensureAsync
)
{
ArgumentNullException.ThrowIfNull(sourceTask);
ArgumentNullException.ThrowIfNull(ensureAsync);

return ThenCore(sourceTask, source => source.ThenEnsureAsync(ensureAsync));
}

/// <summary>
/// Runs a side effect for the successful value of a task-wrapped result while preserving the original result.
/// </summary>
Expand Down Expand Up @@ -554,6 +655,27 @@ Func<TSource, Task<FailOr<TResult>>> bindAsync
return await resultTask.ConfigureAwait(false);
}

private static FailOr<TSource> ThenEnsureCore<TSource, TResult>(
TSource value,
Func<TSource, FailOr<TResult>> ensure
)
{
var ensured = ensure(value);
return ensured.IsFailure ? FailOr.Fail<TSource>(ensured.Failures) : FailOr.Success(value);
}

private static async Task<FailOr<TSource>> ThenEnsureAsyncCore<TSource, TResult>(
TSource value,
Func<TSource, Task<FailOr<TResult>>> ensureAsync
)
{
var resultTask = ensureAsync(value);
ArgumentNullException.ThrowIfNull(resultTask);

var ensured = await resultTask.ConfigureAwait(false);
return ensured.IsFailure ? FailOr.Fail<TSource>(ensured.Failures) : FailOr.Success(value);
}

private static async Task<FailOr<TSource>> ThenDoAsyncCore<TSource>(
TSource value,
Func<TSource, Task> actionAsync
Expand Down
Loading