Skip to content

Commit

Permalink
Merge pull request #77 from amantinband/feature/add-chain-and-chain-a…
Browse files Browse the repository at this point in the history
…sync

Add Then/ThenAsync and Else/ElseAsync
  • Loading branch information
amantinband authored Jan 4, 2024
2 parents 0fcad40 + e2b33d5 commit e2ee67f
Show file tree
Hide file tree
Showing 13 changed files with 906 additions and 383 deletions.
106 changes: 90 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@
</div>

- [Give it a star ⭐!](#give-it-a-star-)
- [Getting Started](#getting-started)
- [Getting Started 🏃](#getting-started-)
- [Single Error](#single-error)
- [This 👇🏽](#this-)
- [Turns into this 👇🏽](#turns-into-this-)
- [This 👇🏽](#this--1)
- [Turns into this 👇🏽](#turns-into-this--1)
- [Multiple Errors](#multiple-errors)
- [A more practical example](#a-more-practical-example)
- [Dropping the exceptions throwing logic](#dropping-the-exceptions-throwing-logic)
- [Usage](#usage)
- [A more practical example 👷](#a-more-practical-example-)
- [Dropping the exceptions throwing logic ✈️](#dropping-the-exceptions-throwing-logic-️)
- [Usage 🛠️](#usage-️)
- [Creating an `ErrorOr<result>`](#creating-an-errororresult)
- [From Value, using implicit conversion](#from-value-using-implicit-conversion)
- [From Value, using `ErrorOrFactory.From`](#from-value-using-errororfactoryfrom)
Expand All @@ -44,21 +44,23 @@
- [`MatchFirst` / `MatchFirstAsync`](#matchfirst--matchfirstasync)
- [`Switch` / `SwitchAsync`](#switch--switchasync)
- [`SwitchFirst` / `SwitchFirstAsync`](#switchfirst--switchfirstasync)
- [`Then` / `ThenAsync`](#then--thenasync)
- [`Else` / `ElseAsync`](#else--elseasync)
- [Error Types](#error-types)
- [Built-in Error Types](#built-in-error-types)
- [Custom error types](#custom-error-types)
- [Why would I want to categorize my errors?](#why-would-i-want-to-categorize-my-errors)
- [Built in result types](#built-in-result-types)
- [How Is This Different From `OneOf<T0, T1>` or `FluentResults`?](#how-is-this-different-from-oneoft0-t1-or-fluentresults)
- [Contribution](#contribution)
- [Credits](#credits)
- [License](#license)
- [How Is This Different From `OneOf<T0, T1>` or `FluentResults`? 🤔](#how-is-this-different-from-oneoft0-t1-or-fluentresults-)
- [Contribution 🤲](#contribution-)
- [Credits 🙏](#credits-)
- [License 🪪](#license-)

# Give it a star ⭐!

Loving it? Show your support by giving this project a star!

# Getting Started
# Getting Started 🏃

## Single Error

Expand Down Expand Up @@ -197,7 +199,7 @@ public async Task<ErrorOr<User>> CreateUserAsync(string name)
}
```

# A more practical example
# A more practical example 👷

```csharp
[HttpGet("{id:guid}")]
Expand Down Expand Up @@ -247,7 +249,7 @@ return createUserResult.MatchFirst(
error => error is Errors.User.DuplicateEmail ? Conflict() : InternalServerError());
```

# Dropping the exceptions throwing logic
# Dropping the exceptions throwing logic ✈️

You have validation logic such as `MediatR` behaviors, you can drop the exceptions throwing logic and simply return a list of errors from the pipeline behavior

Expand Down Expand Up @@ -303,7 +305,7 @@ public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TReques
}
```

# Usage
# Usage 🛠️

## Creating an `ErrorOr<result>`

Expand Down Expand Up @@ -509,6 +511,78 @@ await errorOrString.SwitchFirstAsync(
firstError => { Console.WriteLine(firstError.Description); return Task.CompletedTask; });
```

### `Then` / `ThenAsync`

Multiple methods that return `ErrorOr<T>` can be chained as follows:

```csharp
static ErrorOr<string> ConvertToString(int num) => num.ToString();
static ErrorOr<int> ConvertToInt(string str) => int.Parse(str);

ErrorOr<string> errorOrString = "5";

ErrorOr<string> result = errorOrString
.Then(str => ConvertToInt(str))
.Then(num => ConvertToString(num))
.Then(str => ConvertToInt(str))
.Then(num => ConvertToString(num));
```

```csharp
static ErrorOr<string> ConvertToString(int num) => num.ToString();
static Task<ErrorOr<string>> ConvertToStringAsync(int num) => Task.FromResult(ErrorOrFactory.From(num.ToString()));
static Task<ErrorOr<int>> ConvertToIntAsync(string str) => Task.FromResult(ErrorOrFactory.From(int.Parse(str)));

ErrorOr<string> errorOrString = "5";

ErrorOr<string> result = await errorOrString
.ThenAsync(str => ConvertToIntAsync(str))
.ThenAsync(num => ConvertToStringAsync(num))
.ThenAsync(str => ConvertToIntAsync(str))
.ThenAsync(num => ConvertToStringAsync(num));

// mixing `ThenAsync` and `Then`
ErrorOr<string> result = await errorOrString
.ThenAsync(str => ConvertToIntAsync(str))
.Then(num => ConvertToString(num))
.ThenAsync(str => ConvertToIntAsync(str))
.Then(num => ConvertToString(num));
```

If any of the methods return an error, the chain will break and the errors will be returned.

### `Else` / `ElseAsync`

The `Else` / `ElseAsync` methods can be used to specify a fallback value in case the state is error anywhere in the chain.

```csharp
// ignoring the errors
string result = errorOrString
.Then(str => ConvertToInt(str))
.Then(num => ConvertToString(num))
.Else("fallback value");

// using the errors
string result = errorOrString
.Then(str => ConvertToInt(str))
.Then(num => ConvertToString(num))
.Else(errors => $"{errors.Count} errors occurred.");
```

```csharp
// ignoring the errors
string result = await errorOrString
.ThenAsync(str => ConvertToInt(str))
.ThenAsync(num => ConvertToString(num))
.ElseAsync(Task.FromResult("fallback value"));

// using the errors
string result = await errorOrString
.ThenAsync(str => ConvertToInt(str))
.ThenAsync(num => ConvertToString(num))
.ElseAsync(errors => Task.FromResult($"{errors.Count} errors occurred."));
```

## Error Types

### Built-in Error Types
Expand Down Expand Up @@ -599,19 +673,19 @@ ErrorOr<Deleted> DeleteUser(Guid id)
}
```

# How Is This Different From `OneOf<T0, T1>` or `FluentResults`?
# How Is This Different From `OneOf<T0, T1>` or `FluentResults`? 🤔

It's similar to the others, just aims to be more intuitive and fluent.
If you find yourself typing `OneOf<User, DomainError>` or `Result.Fail<User>("failure")` again and again, you might enjoy the fluent API of `ErrorOr<User>` (and it's also faster).

# Contribution
# Contribution 🤲

If you have any questions, comments, or suggestions, please open an issue or create a pull request 🙂

# Credits
# Credits 🙏

- [OneOf](https://github.com/mcintyre321/OneOf/tree/master/OneOf) - An awesome library which provides F# style discriminated unions behavior for C#
# License
# License 🪪

This project is licensed under the terms of the [MIT](https://github.com/mantinband/error-or/blob/main/LICENSE) license.
99 changes: 87 additions & 12 deletions src/ErrorOr.cs
Original file line number Diff line number Diff line change
Expand Up @@ -259,21 +259,96 @@ public async Task<TResult> MatchFirstAsync<TResult>(Func<TValue, Task<TResult>>

return await onValue(Value).ConfigureAwait(false);
}
}

/// <summary>
/// Provides utility methods for creating instances of <see ref="ErrorOr{T}"/>.
/// </summary>
public static class ErrorOr
{
/// <summary>
/// Creates an <see ref="ErrorOr{TValue}"/> instance from a value.
/// If the state is a value, the provided function <paramref name="onValue"/> is executed and its result is returned.
/// </summary>
/// <typeparam name="TValue">The type of the value.</typeparam>
/// <param name="value">The value from which to create an ErrorOr instance.</param>
/// <returns>An <see ref="ErrorOr{TValue}"/> instance containing the specified value.</returns>
public static ErrorOr<TValue> From<TValue>(TValue value)
/// <typeparam name="TResult">The type of the result.</typeparam>
/// <param name="onValue">The function to execute if the state is a value.</param>
/// <returns>The result from calling <paramref name="onValue"/> if state is value; otherwise the original <see cref="Errors"/>.</returns>
public ErrorOr<TResult> Then<TResult>(Func<TValue, ErrorOr<TResult>> onValue)
{
return value;
if (IsError)
{
return Errors;
}

return onValue(Value);
}

/// <summary>
/// If the state is a value, the provided function <paramref name="onValue"/> is executed asynchronously and its result is returned.
/// </summary>
/// <typeparam name="TResult">The type of the result.</typeparam>
/// <param name="onValue">The function to execute if the state is a value.</param>
/// <returns>The result from calling <paramref name="onValue"/> if state is value; otherwise the original <see cref="Errors"/>.</returns>
public async Task<ErrorOr<TResult>> ThenAsync<TResult>(Func<TValue, Task<ErrorOr<TResult>>> onValue)
{
if (IsError)
{
return Errors;
}

return await onValue(Value).ConfigureAwait(false);
}

/// <summary>
/// If the state is error, the provided function <paramref name="onError"/> is executed and its result is returned.
/// </summary>
/// <param name="onError">The function to execute if the state is error.</param>
/// <returns>The result from calling <paramref name="onError"/> if state is error; otherwise the original <see cref="Value"/>.</returns>
public TValue Else(Func<List<Error>, TValue> onError)
{
if (!IsError)
{
return Value;
}

return onError(Errors);
}

/// <summary>
/// If the state is error, the provided function <paramref name="onError"/> is executed and its result is returned.
/// </summary>
/// <param name="onError">The value to return if the state is error.</param>
/// <returns>The result from calling <paramref name="onError"/> if state is error; otherwise the original <see cref="Value"/>.</returns>
public TValue Else(TValue onError)
{
if (!IsError)
{
return Value;
}

return onError;
}

/// <summary>
/// If the state is error, the provided function <paramref name="onError"/> is executed asynchronously and its result is returned.
/// </summary>
/// <param name="onError">The function to execute if the state is error.</param>
/// <returns>The result from calling <paramref name="onError"/> if state is error; otherwise the original <see cref="Value"/>.</returns>
public async Task<TValue> ElseAsync(Func<List<Error>, Task<TValue>> onError)
{
if (!IsError)
{
return Value;
}

return await onError(Errors).ConfigureAwait(false);
}

/// <summary>
/// If the state is error, the provided function <paramref name="onError"/> is executed asynchronously and its result is returned.
/// </summary>
/// <param name="onError">The function to execute if the state is error.</param>
/// <returns>The result from calling <paramref name="onError"/> if state is error; otherwise the original <see cref="Value"/>.</returns>
public async Task<TValue> ElseAsync(Task<TValue> onError)
{
if (!IsError)
{
return Value;
}

return await onError.ConfigureAwait(false);
}
}
86 changes: 86 additions & 0 deletions src/ErrorOrExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
namespace ErrorOr;

public static class ErrorOrExtensions
{
/// <summary>
/// If the state of <paramref name="errorOr"/> is a value, the provided function <paramref name="onValue"/> is executed and its result is returned.
/// </summary>
/// <typeparam name="TResult">The type of the result.</typeparam>
/// <typeparam name="TNextResult">The type of the next result.</typeparam>
/// <param name="errorOr">The error.</param>
/// <param name="onValue">The function to execute if the state is a value.</param>
/// <returns>The result from calling <paramref name="onValue"/> if state is value; otherwise the original errors.</returns>
public static async Task<ErrorOr<TNextResult>> Then<TResult, TNextResult>(this Task<ErrorOr<TResult>> errorOr, Func<TResult, ErrorOr<TNextResult>> onValue)
{
var result = await errorOr.ConfigureAwait(false);

return result.Then(onValue);
}

/// <summary>
/// If the state of <paramref name="errorOr"/> is a value, the provided function <paramref name="onValue"/> is executed asynchronously and its result is returned.
/// </summary>
/// <typeparam name="TResult">The type of the result.</typeparam>
/// <typeparam name="TNextResult">The type of the next result.</typeparam>
/// <param name="errorOr">The error.</param>
/// <param name="onValue">The function to execute if the state is a value.</param>
/// <returns>The result from calling <paramref name="onValue"/> if state is value; otherwise the original errors.</returns>
public static async Task<ErrorOr<TNextResult>> ThenAsync<TResult, TNextResult>(this Task<ErrorOr<TResult>> errorOr, Func<TResult, Task<ErrorOr<TNextResult>>> onValue)
{
var result = await errorOr.ConfigureAwait(false);

return await result.ThenAsync(onValue).ConfigureAwait(false);
}

/// <summary>
/// If the state is error, the provided function <paramref name="onError"/> is executed asynchronously and its result is returned.
/// </summary>
/// <param name="errorOr">The error.</param>
/// <param name="onError">The function to execute if the state is error.</param>
/// <returns>The result from calling <paramref name="onError"/> if state is error; otherwise the original value.</returns>
public static async Task<TValue> Else<TValue>(this Task<ErrorOr<TValue>> errorOr, Func<List<Error>, TValue> onError)
{
var result = await errorOr.ConfigureAwait(false);

return result.Else(onError);
}

/// <summary>
/// If the state is error, the provided function <paramref name="onError"/> is executed asynchronously and its result is returned.
/// </summary>
/// <param name="errorOr">The error.</param>
/// <param name="onError">The function to execute if the state is error.</param>
/// <returns>The result from calling <paramref name="onError"/> if state is error; otherwise the original value.</returns>
public static async Task<TValue> Else<TValue>(this Task<ErrorOr<TValue>> errorOr, TValue onError)
{
var result = await errorOr.ConfigureAwait(false);

return result.Else(onError);
}

/// <summary>
/// If the state is error, the provided function <paramref name="onError"/> is executed asynchronously and its result is returned.
/// </summary>
/// <param name="errorOr">The error.</param>
/// <param name="onError">The function to execute if the state is error.</param>
/// <returns>The result from calling <paramref name="onError"/> if state is error; otherwise the original value.</returns>
public static async Task<TValue> ElseAsync<TValue>(this Task<ErrorOr<TValue>> errorOr, Func<List<Error>, Task<TValue>> onError)
{
var result = await errorOr.ConfigureAwait(false);

return await result.ElseAsync(onError).ConfigureAwait(false);
}

/// <summary>
/// If the state is error, the provided function <paramref name="onError"/> is executed asynchronously and its result is returned.
/// </summary>
/// <param name="errorOr">The error.</param>
/// <param name="onError">The function to execute if the state is error.</param>
/// <returns>The result from calling <paramref name="onError"/> if state is error; otherwise the original value.</returns>
public static async Task<TValue> ElseAsync<TValue>(this Task<ErrorOr<TValue>> errorOr, Task<TValue> onError)
{
var result = await errorOr.ConfigureAwait(false);

return await result.ElseAsync(onError).ConfigureAwait(false);
}
}
Loading

0 comments on commit e2ee67f

Please sign in to comment.