Skip to content

Commit

Permalink
Merge pull request #90 from amantinband/error-or-2.0
Browse files Browse the repository at this point in the history
ErrorOr 2.0
  • Loading branch information
amantinband authored Mar 26, 2024
2 parents 605e497 + df64c18 commit f7ed142
Show file tree
Hide file tree
Showing 21 changed files with 832 additions and 577 deletions.
52 changes: 47 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,57 @@
# Changelog

All notable changes to this project will be documented in this file.
All notable changes to this project are documented in this file.

## [1.10.0] - 2024-02-14

### Added
- Forbidden Error Type

- `ErrorType.Forbidden`
- README to NuGet package

## [1.9.0] - 2024-01-06

### Added

- `ToErrorOr`

## [2.0.0] - 2024-03-26

### Added
- ToErrorOr

### Fixed
- Added Missing tests
- `FailIf`

```csharp
public ErrorOr<TValue> FailIf(Func<TValue, bool> onValue, Error error)
```

```csharp
ErrorOr<int> errorOr = 1;
errorOr.FailIf(x => x > 0, Error.Failure());
```

### Breaking Changes

- `Then` that receives an action is now called `ThenDo`

```diff
-public ErrorOr<TValue> Then(Action<TValue> action)
+public ErrorOr<TValue> ThenDo(Action<TValue> action)
```

```diff
-public static async Task<ErrorOr<TValue>> Then<TValue>(this Task<ErrorOr<TValue>> errorOr, Action<TValue> action)
+public static async Task<ErrorOr<TValue>> ThenDo<TValue>(this Task<ErrorOr<TValue>> errorOr, Action<TValue> action)
```

- `ThenAsync` that receives an action is now called `ThenDoAsync`

```diff
-public async Task<ErrorOr<TValue>> ThenAsync(Func<TValue, Task> action)
+public async Task<ErrorOr<TValue>> ThenDoAsync(Func<TValue, Task> action)
```

```diff
-public static async Task<ErrorOr<TValue>> ThenAsync<TValue>(this Task<ErrorOr<TValue>> errorOr, Func<TValue, Task> action)
+public static async Task<ErrorOr<TValue>> ThenDoAsync<TValue>(this Task<ErrorOr<TValue>> errorOr, Func<TValue, Task> action)
```
42 changes: 21 additions & 21 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>

<Project>

<PropertyGroup>
<LangVersion>10.0</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>

</Project>
<?xml version="1.0" encoding="utf-8"?>

<Project>

<PropertyGroup>
<LangVersion>10.0</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>

</Project>
135 changes: 119 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@
- [Give it a star ⭐!](#give-it-a-star-)
- [Getting Started 🏃](#getting-started-)
- [Replace throwing exceptions with `ErrorOr<T>`](#replace-throwing-exceptions-with-errorort)
- [Return Multiple Errors When Needed](#return-multiple-errors-when-needed)
- [Support For Multiple Errors](#support-for-multiple-errors)
- [Various Functional Methods and Extension Methods](#various-functional-methods-and-extension-methods)
- [Real world example](#real-world-example)
- [Simple Example with intermediate steps](#simple-example-with-intermediate-steps)
- [No Failure](#no-failure)
- [Failure](#failure)
- [Creating an `ErrorOr` instance](#creating-an-erroror-instance)
- [Using implicit conversion](#using-implicit-conversion)
- [Using The `ErrorOrFactory`](#using-the-errororfactory)
Expand All @@ -44,11 +49,13 @@
- [`Then`](#then)
- [`Then`](#then-1)
- [`ThenAsync`](#thenasync)
- [Mixing `Then` and `ThenAsync`](#mixing-then-and-thenasync)
- [`ThenDo` and `ThenDoAsync`](#thendo-and-thendoasync)
- [Mixing `Then`, `ThenDo`, `ThenAsync`, `ThenDoAsync`](#mixing-then-thendo-thenasync-thendoasync)
- [`FailIf`](#failif)
- [`Else`](#else)
- [`Else`](#else-1)
- [`ElseAsync`](#elseasync)
- [Mixing Features (`Then`, `Else`, `Switch`, `Match`)](#mixing-features-then-else-switch-match)
- [Mixing Features (`Then`, `FailIf`, `Else`, `Switch`, `Match`)](#mixing-features-then-failif-else-switch-match)
- [Error Types](#error-types)
- [Built in error types](#built-in-error-types)
- [Custom error types](#custom-error-types)
Expand Down Expand Up @@ -127,7 +134,7 @@ Divide(4, 2)
onFirstError: error => Console.WriteLine(error.Description));
```

## Return Multiple Errors When Needed
## Support For Multiple Errors

Internally, the `ErrorOr` object has a list of `Error`s, so if you have multiple errors, you don't need to compromise and have only the first one.

Expand Down Expand Up @@ -163,6 +170,62 @@ public class User(string _name)
}
```

## Various Functional Methods and Extension Methods

The `ErrorOr` object has a variety of methods that allow you to work with it in a functional way.

This allows you to chain methods together, and handle the result in a clean and concise way.

### Real world example

```cs
return await _userRepository.GetByIdAsync(id)
.Then(user => user.IncrementAge()
.Then(success => user)
.Else(errors => Error.Unexpected("Not expected to fail")))
.FailIf(user => !user.IsOverAge(18), UserErrors.UnderAge)
.ThenDo(user => _logger.LogInformation($"User {user.Id} incremented age to {user.Age}"))
.ThenAsync(user => _userRepository.UpdateAsync(user))
.Match(
_ => NoContent(),
errors => errors.ToActionResult());
```

### Simple Example with intermediate steps

#### No Failure

```cs
ErrorOr<string> foo = await "2".ToErrorOr()
.Then(int.Parse) // 2
.FailIf(val => val > 2, Error.Validation(description: $"{val} is too big") // 2
.ThenDoAsync(Task.Delay) // Sleep for 2 milliseconds
.ThenDo(val => Console.WriteLine($"Finished waiting {val} milliseconds.")) // Finished waiting 2 milliseconds.
.ThenAsync(val => Task.FromResult(val * 2)) // 4
.Then(val => $"The result is {val}") // "The result is 4"
.Else(errors => Error.Unexpected(description: "Yikes")) // "The result is 4"
.MatchFirst(
value => value, // "The result is 4"
firstError => $"An error occurred: {firstError.Description}");
```

#### Failure

```cs
ErrorOr<string> foo = await "5".ToErrorOr()
.Then(int.Parse) // 5
.FailIf(val => val > 2, Error.Validation(description: $"{val} is too big") // Error.Validation()
.ThenDoAsync(Task.Delay) // Error.Validation()
.ThenDo(val => Console.WriteLine($"Finished waiting {val} milliseconds.")) // Error.Validation()
.ThenAsync(val => Task.FromResult(val * 2)) // Error.Validation()
.Then(val => $"The result is {val}") // Error.Validation()
.Else(errors => Error.Unexpected(description: "Yikes")) // Error.Unexpected()
.MatchFirst(
value => value,
firstError => $"An error occurred: {firstError.Description}"); // An error occurred: 5 is too big
```


# Creating an `ErrorOr` instance

## Using implicit conversion
Expand Down Expand Up @@ -388,7 +451,7 @@ await result.SwitchFirstAsync(

### `Then`

`Then` receives an action or a function, and invokes it only if the result is not an error.
`Then` receives a function, and invokes it only if the result is not an error.

```cs
ErrorOr<int> foo = result
Expand Down Expand Up @@ -417,26 +480,65 @@ ErrorOr<string> foo = result

### `ThenAsync`

`ThenAsync` receives an asynchronous action or function, and invokes it only if the result is not an error.
`ThenAsync` receives an asynchronous function, and invokes it only if the result is not an error.

```cs
ErrorOr<string> foo = await result
.ThenAsync(val => Task.Delay(val))
.ThenAsync(val => Task.FromResult($"The result is {val}"));
.ThenAsync(val => DoSomethingAsync(val))
.ThenAsync(val => DoSomethingElseAsync($"The result is {val}"));
```

### Mixing `Then` and `ThenAsync`
### `ThenDo` and `ThenDoAsync`

You can mix `Then` and `ThenAsync` methods together.
`ThenDo` and `ThenDoAsync` are similar to `Then` and `ThenAsync`, but instead of invoking a function that returns a value, they invoke an action.

```cs
ErrorOr<string> foo = result
.ThenDo(val => Console.WriteLine(val))
.ThenDo(val => Console.WriteLine($"The result is {val}"));
```

```cs
ErrorOr<string> foo = await result
.ThenAsync(val => Task.Delay(val))
.Then(val => Console.WriteLine($"Finsihed waiting {val} seconds."))
.ThenDoAsync(val => Task.Delay(val))
.ThenDo(val => Console.WriteLine($"Finsihed waiting {val} seconds."))
.ThenDoAsync(val => Task.FromResult(val * 2))
.ThenDo(val => $"The result is {val}");
```

### Mixing `Then`, `ThenDo`, `ThenAsync`, `ThenDoAsync`

You can mix and match `Then`, `ThenDo`, `ThenAsync`, `ThenDoAsync` methods.

```cs
ErrorOr<string> foo = await result
.ThenDoAsync(val => Task.Delay(val))
.Then(val => val * 2)
.ThenAsync(val => DoSomethingAsync(val))
.ThenDo(val => Console.WriteLine($"Finsihed waiting {val} seconds."))
.ThenAsync(val => Task.FromResult(val * 2))
.Then(val => $"The result is {val}");
```

## `FailIf`

`FailIf` receives a predicate and an error. If the predicate is true, `FailIf` will return the error. Otherwise, it will return the value of the result.

```cs
ErrorOr<int> foo = result
.FailIf(val => val > 2, Error.Validation(description: $"{val} is too big"));
```

Once an error is returned, the chain will break and the error will be returned.

```cs
var result = "2".ToErrorOr()
.Then(int.Parse) // 2
.FailIf(val => val > 1, Error.Validation(description: $"{val} is too big") // validation error
.Then(num => num * 2) // this function will not be invoked
.Then(num => num * 2) // this function will not be invoked
```

## `Else`

`Else` receives a value or a function. If the result is an error, `Else` will return the value or invoke the function. Otherwise, it will return the value of the result.
Expand Down Expand Up @@ -465,14 +567,15 @@ ErrorOr<string> foo = await result
.ElseAsync(errors => Task.FromResult($"{errors.Count} errors occurred."));
```

# Mixing Features (`Then`, `Else`, `Switch`, `Match`)
# Mixing Features (`Then`, `FailIf`, `Else`, `Switch`, `Match`)

You can mix `Then`, `Else`, `Switch` and `Match` methods together.
You can mix `Then`, `FailIf`, `Else`, `Switch` and `Match` methods together.

```cs
ErrorOr<string> foo = await result
.ThenAsync(val => Task.Delay(val))
.Then(val => Console.WriteLine($"Finsihed waiting {val} seconds."))
.ThenDoAsync(val => Task.Delay(val))
.FailIf(val => val > 2, Error.Validation(description: $"{val} is too big"))
.ThenDo(val => Console.WriteLine($"Finished waiting {val} seconds."))
.ThenAsync(val => Task.FromResult(val * 2))
.Then(val => $"The result is {val}")
.Else(errors => Error.Unexpected())
Expand Down
Loading

0 comments on commit f7ed142

Please sign in to comment.