From a757b15e814d6bdea264869ba2ca0f468393e049 Mon Sep 17 00:00:00 2001 From: Amichai Mantinband Date: Sat, 6 Jan 2024 16:21:22 +0200 Subject: [PATCH] Add ToErrorOr, Updated Docs & change else to return ErrorOr --- README.md | 637 ++++++++++++++++------------- src/ErrorOr.ElseExtensions.cs | 92 ++++- src/ErrorOr.ToErrorOrExtensions.cs | 28 ++ src/ErrorOr.cs | 98 ++++- src/ErrorOrFactory.cs | 58 ++- tests/ErrorOr.ElseAsyncTests.cs | 130 +++++- tests/ErrorOr.ElseTests.cs | 144 ++++++- tests/ErrorOr.ToErrorOrTests.cs | 49 +++ 8 files changed, 889 insertions(+), 347 deletions(-) create mode 100644 src/ErrorOr.ToErrorOrExtensions.cs create mode 100644 tests/ErrorOr.ToErrorOrTests.cs diff --git a/README.md b/README.md index 49a94ea..cbb5565 100644 --- a/README.md +++ b/README.md @@ -20,31 +20,41 @@ - [Getting Started 🏃](#getting-started-) - [Replace throwing exceptions with `ErrorOr`](#replace-throwing-exceptions-with-errorort) - [Return Multiple Errors When Needed](#return-multiple-errors-when-needed) - - [`ErrorOr` in ASP.NET](#errorort-in-aspnet) - - [Organizing your errors](#organizing-your-errors) +- [Creating an `ErrorOr` instance](#creating-an-erroror-instance) + - [Using implicit conversion](#using-implicit-conversion) + - [Using The `ErrorOrFactory`](#using-the-errororfactory) + - [Using The `ToErrorOr` Extension Method](#using-the-toerroror-extension-method) +- [Properties](#properties) + - [`IsError`](#iserror) + - [`Value`](#value) + - [`Errors`](#errors) + - [`FirstError`](#firsterror) + - [`ErrorsOrEmptyList`](#errorsoremptylist) +- [Methods](#methods) + - [`Match`](#match) + - [`Match`](#match-1) + - [`MatchAsync`](#matchasync) + - [`MatchFirst`](#matchfirst) + - [`MatchFirstAsync`](#matchfirstasync) + - [`Switch`](#switch) + - [`Switch`](#switch-1) + - [`SwitchAsync`](#switchasync) + - [`SwitchFirst`](#switchfirst) + - [`SwitchFirstAsync`](#switchfirstasync) + - [`Then`](#then) + - [`Then`](#then-1) + - [`ThenAsync`](#thenasync) + - [Mixing `Then` and `ThenAsync`](#mixing-then-and-thenasync) + - [`Else`](#else) + - [`Else`](#else-1) + - [`ElseAsync`](#elseasync) +- [Mixing Features (`Then`, `Else`, `Switch`, `Match`)](#mixing-features-then-else-switch-match) +- [Error Types](#error-types) + - [Built in error types](#built-in-error-types) + - [Custom error types](#custom-error-types) +- [Built in result types (`Result.Success`, ..)](#built-in-result-types-resultsuccess-) +- [Organizing Errors](#organizing-errors) - [Mediator + FluentValidation + `ErrorOr` 🤝](#mediator--fluentvalidation--erroror-) -- [Usage 🛠️](#usage-️) - - [Creating an `ErrorOr`](#creating-an-errorortresult) - - [From Value, using implicit conversion](#from-value-using-implicit-conversion) - - [From Value, using `ErrorOrFactory.From`](#from-value-using-errororfactoryfrom) - - [From Single Error](#from-single-error) - - [From List of Errors, using implicit conversion](#from-list-of-errors-using-implicit-conversion) - - [Accessing the List of Errors (`result.Errors`)](#accessing-the-list-of-errors-resulterrors) - - [Accessing the First Error (`result.FirstError`)](#accessing-the-first-error-resultfirsterror) - - [Accessing the Errors or an empty list (`result.ErrorsOrEmptyList`)](#accessing-the-errors-or-an-empty-list-resulterrorsoremptylist) - - [Performing actions based on the `ErrorOr` result](#performing-actions-based-on-the-errororresult-result) - - [`Match` / `MatchAsync`](#match--matchasync) - - [`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` or `FluentResults`? 🤔](#how-is-this-different-from-oneoft0-t1-or-fluentresults-) - [Contribution 🤲](#contribution-) - [Credits 🙏](#credits-) - [License 🪪](#license-) @@ -60,6 +70,16 @@ Loving it? Show your support by giving this project a star! This 👇 ```cs +public float Divide(int a, int b) +{ + if (b == 0) + { + throw new Exception("Cannot divide by zero"); + } + + return a / b; +} + try { var result = Divide(4, 2); @@ -70,21 +90,21 @@ catch (Exception e) Console.WriteLine(e.Message); return; } +``` -public float Divide(int a, int b) +Turns into this 👇 + +```cs +public ErrorOr Divide(int a, int b) { if (b == 0) { - throw new Exception("Cannot divide by zero"); + return Error.Unexpected(description: "Cannot divide by zero"); } return a / b; } -``` -Turns into this 👇 - -```cs var result = Divide(4, 2); if (result.IsError) @@ -94,16 +114,6 @@ if (result.IsError) } Console.WriteLine(result.Value * 2); // 4 - -public ErrorOr Divide(int a, int b) -{ - if (b == 0) - { - return Error.Unexpected(description: "Cannot divide by zero"); - } - - return a / b; -} ``` Or, using [Then](#then--thenasync)/[Else](#else--elseasync) and [Switch](#switch--switchasync)/[Match](#match--matchasync), you can do this 👇 @@ -111,19 +121,17 @@ Or, using [Then](#then--thenasync)/[Else](#else--elseasync) and [Switch](#switch ```cs Divide(4, 2) - .Then(val => val * 2) // you can chain multiple `Then` methods. They will only be invoked if the result is not an error + .Then(val => val * 2) .SwitchFirst( onValue: Console.WriteLine, // 4 onFirstError: error => Console.WriteLine(error.Description)); - -public ErrorOr Divide(int a, int b); // same as above ``` ## Return Multiple Errors When Needed 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. -```csharp +```cs public class User(string _name) { public static ErrorOr Create(string name) @@ -155,354 +163,333 @@ public class User(string _name) } ``` -## `ErrorOr` in ASP.NET +# Creating an `ErrorOr` instance -```csharp -[HttpGet("{id:guid}")] -public async Task GetUser(Guid Id) -{ - var getUserQuery = new GetUserQuery(Id); +## Using implicit conversion - ErrorOr getUserResponse = await _mediator.Send(getUserQuery); +There are implicit converters from `TResult`, `Error`, `List` to `ErrorOr` - return getUserResponse - .Then(user => _mapper.Map(user)) // will only be invoked if the result is not an error - .Match(onValue: Ok, onError: Problem); -} +```cs +ErrorOr result = 5; +ErrorOr result = Error.Unexpected(); +ErrorOr result = [Error.Validation(), Error.Validation()]; ``` -## Organizing your errors - -A nice approach, is creating a static class with the expected errors. For example: - -```csharp -public static partial class DivisionErrors +```cs +public ErrorOr IntToErrorOr() { - public static Error CannotDivideByZero = Error.Unexpected( - code: "Division.CannotDivideByZero", - description: "Cannot divide by zero."); + return 5; } ``` -Which can later be used as following 👇 - -```csharp -public ErrorOr Divide(int a, int b) +```cs +public ErrorOr SingleErrorToErrorOr() { - if (b == 0) - { - return DivisionErrors.CannotDivideByZero; - } - - return a / b; + return Error.Unexpected(); } ``` -Then, in an outer layer, you can use the `Error.Match` method to return the appropriate HTTP status code. - -```csharp -return Divide(a, b) - .MatchFirst( - onValue: user => CreatedAtRoute("GetUser", new { id = user.Id }, user), - onError: error => error is DivisionErrors.CannotDivideByZero ? BadRequest() : InternalServerError()); -``` - -# [Mediator](https://github.com/jbogard/MediatR) + [FluentValidation](https://github.com/FluentValidation/FluentValidation) + `ErrorOr` 🤝 - -A common approach when using `MediatR` is to use `FluentValidation` to validate the request before it reaches the handler. - -Usually, the validation is done using a `Behavior` that throws an exception if the request is invalid. - -Using `ErrorOr`, we can create a `Behavior` that returns an error instead of throwing an exception. - -This plays nicely when the project uses `ErrorOr`, as the layer invoking the `Mediator`, similar to other components in the project, simply receives an `ErrorOr` and can handle it accordingly. - -Here is an example of a `Behavior` that validates the request and returns an error if it's invalid 👇 - -```csharp -public class ValidationBehavior(IValidator? validator = null) - : IPipelineBehavior - where TRequest : IRequest - where TResponse : IErrorOr +```cs +public ErrorOr MultipleErrorsToErrorOr() { - private readonly IValidator? _validator = validator; - - public async Task Handle( - TRequest request, - RequestHandlerDelegate next, - CancellationToken cancellationToken) - { - if (_validator is null) - { - return await next(); - } - - var validationResult = await _validator.ValidateAsync(request, cancellationToken); - - if (validationResult.IsValid) - { - return await next(); - } - - var errors = validationResult.Errors - .ConvertAll(error => Error.Validation( - code: error.PropertyName, - description: error.ErrorMessage)); - - return (dynamic)errors; - } + return [ + Error.Validation(description: "Invalid Name"), + Error.Validation(description: "Invalid Last Name") + ]; } ``` -# Usage 🛠️ - -## Creating an `ErrorOr` - -There are implicit converters from `TResult`, `Error`, `List` to `ErrorOr` +## Using The `ErrorOrFactory` -### From Value, using implicit conversion - -```csharp -ErrorOr result = 5; +```cs +ErrorOr result = ErrorOrFactory.From(5); +ErrorOr result = ErrorOrFactory.From(Error.Unexpected()); +ErrorOr result = ErrorOrFactory.From([Error.Validation(), Error.Validation()]); ``` -```csharp +```cs public ErrorOr GetValue() { - return 5; + return ErrorOrFactory.From(5); } ``` -### From Value, using `ErrorOrFactory.From` - -```csharp -ErrorOr result = ErrorOrFactory.From(5); +```cs +public ErrorOr SingleErrorToErrorOr() +{ + return ErrorOrFactory.From(Error.Unexpected()); +} ``` -```csharp -public ErrorOr GetValue() +```cs +public ErrorOr MultipleErrorsToErrorOr() { - return ErrorOrFactory.From(5); + return ErrorOrFactory.From([ + Error.Validation(description: "Invalid Name"), + Error.Validation(description: "Invalid Last Name") + ]); } ``` -### From Single Error +## Using The `ToErrorOr` Extension Method -```csharp -ErrorOr result = Error.Unexpected(me); +```cs +ErrorOr result = 5.ToErrorOr(); +ErrorOr result = Error.Unexpected().ToErrorOr(); +ErrorOr result = new[] { Error.Validation(), Error.Validation() }.ToErrorOr(); ``` -```csharp -public ErrorOr GetValue() -{ - return Error.Unexpected(); -} -``` +# Properties -### From List of Errors, using implicit conversion +## `IsError` -```csharp -ErrorOr result = new List { Error.Unexpected(), Error.Validation() }; -``` +```cs +ErrorOr foo = User.Create(); -```csharp -public ErrorOr GetValue() +if (result.IsError) { - return new List - { - Error.Unexpected(), - Error.Validation() - }; + // the result contains one or more errors } ``` -### Accessing the List of Errors (`result.Errors`) +## `Value` -```csharp -ErrorOr result = new List { Error.Unexpected(), Error.Validation() }; +```cs +ErrorOr foo = User.Create(); -List value = result.Errors; // List { Error.Unexpected(), Error.Validation() } +if (!result.IsError) // the result contains a value +{ + Console.WriteLine(result.Value); +} ``` -```csharp -ErrorOr result = Error.Unexpected(); +## `Errors` + +```cs +ErrorOr foo = User.Create(); -List value = result.Errors; // List { Error.Unexpected() } +if (result.IsError) +{ + result.Errors // contains the list of errors that occurred + .ForEach(error => Console.WriteLine(error.Description)); +} ``` -### Accessing the First Error (`result.FirstError`) +## `FirstError` -```csharp -ErrorOr result = new List { Error.Unexpected(), Error.Validation() }; +```cs +ErrorOr foo = User.Create(); -Error value = result.FirstError; // Error.Unexpected() +if (result.IsError) +{ + var firstError = result.FirstError; // only the first error that occurred + Console.WriteLine(firstError == result.Errors[0]); // true +} ``` -```csharp -ErrorOr result = Error.Unexpected(); - -Error value = result.FirstError; // Error.Unexpected() -``` +## `ErrorsOrEmptyList` -### Accessing the Errors or an empty list (`result.ErrorsOrEmptyList`) +```cs +ErrorOr foo = User.Create(); -```csharp -ErrorOr result = new List { Error.Unexpected(), Error.Validation() }; +if (result.IsError) +{ + result.ErrorsOrEmptyList // List { /* one or more errors */ } + return; +} -List errors = result.ErrorsOrEmptyList; // List { Error.Unexpected(), Error.Validation() } +result.ErrorsOrEmptyList // List { } ``` -```csharp -ErrorOr result = ErrorOrFactory.From(5); +# Methods -List errors = result.ErrorsOrEmptyList; // List { } -``` +## `Match` -## Performing actions based on the `ErrorOr` result +The `Match` method receives two functions, `onValue` and `onError`, `onValue` will be invoked if the result is success, and `onError` is invoked if the result is an error. -### `Match` / `MatchAsync` +### `Match` -Actions that return a value on the value or list of errors - -```csharp -string foo = errorOrString.Match( +```cs +string foo = result.Match( value => value, errors => $"{errors.Count} errors occurred."); ``` -```csharp -string foo = await errorOrString.MatchAsync( +### `MatchAsync` + +```cs +string foo = await result.MatchAsync( value => Task.FromResult(value), errors => Task.FromResult($"{errors.Count} errors occurred.")); ``` -### `MatchFirst` / `MatchFirstAsync` +### `MatchFirst` + +The `MatchFirst` method receives two functions, `onValue` and `onError`, `onValue` will be invoked if the result is success, and `onError` is invoked if the result is an error. + +Unlike `Match`, if the state is error, `MatchFirst`'s `onError` function receives only the first error that occurred, not the entire list of errors. -Actions that return a value on the value or first error -```csharp -string foo = errorOrString.MatchFirst( +```cs +string foo = result.MatchFirst( value => value, firstError => firstError.Description); ``` -```csharp -string foo = await errorOrString.MatchFirstAsync( +### `MatchFirstAsync` + +```cs +string foo = await result.MatchFirstAsync( value => Task.FromResult(value), firstError => Task.FromResult(firstError.Description)); ``` -### `Switch` / `SwitchAsync` +## `Switch` -Actions that don't return a value on the value or list of errors +The `Switch` method receives two actions, `onValue` and `onError`, `onValue` will be invoked if the result is success, and `onError` is invoked if the result is an error. -```csharp -errorOrString.Switch( +### `Switch` + +```cs +result.Switch( value => Console.WriteLine(value), errors => Console.WriteLine($"{errors.Count} errors occurred.")); ``` -```csharp -await errorOrString.SwitchAsync( +### `SwitchAsync` + +```cs +await result.SwitchAsync( value => { Console.WriteLine(value); return Task.CompletedTask; }, errors => { Console.WriteLine($"{errors.Count} errors occurred."); return Task.CompletedTask; }); ``` -### `SwitchFirst` / `SwitchFirstAsync` +### `SwitchFirst` -Actions that don't return a value on the value or first error +The `SwitchFirst` method receives two actions, `onValue` and `onError`, `onValue` will be invoked if the result is success, and `onError` is invoked if the result is an error. -```csharp -errorOrString.SwitchFirst( +Unlike `Switch`, if the state is error, `SwitchFirst`'s `onError` function receives only the first error that occurred, not the entire list of errors. + +```cs +result.SwitchFirst( value => Console.WriteLine(value), firstError => Console.WriteLine(firstError.Description)); ``` -```csharp -await errorOrString.SwitchFirstAsync( +### `SwitchFirstAsync` + +```cs +await result.SwitchFirstAsync( value => { Console.WriteLine(value); return Task.CompletedTask; }, firstError => { Console.WriteLine(firstError.Description); return Task.CompletedTask; }); ``` -### `Then` / `ThenAsync` +## `Then` + +### `Then` -Multiple methods that return `ErrorOr` can be chained as follows: +`Then` receives an action or a function, and invokes it only if the result is not an error. -```csharp -static ErrorOr ConvertToString(int num) => num.ToString(); -static ErrorOr ConvertToInt(string str) => int.Parse(str); +```cs +ErrorOr foo = result + .Then(val => val * 2); +``` -ErrorOr errorOrString = "5"; +Multiple `Then` methods can be chained together. -ErrorOr result = errorOrString - .Then(str => ConvertToInt(str)) - .Then(num => ConvertToString(num)) - .Then(str => ConvertToInt(str)) - .Then(num => ConvertToString(num)); +```cs +ErrorOr foo = result + .Then(val => val * 2) + .Then(val => $"The result is {val}"); ``` -```csharp -static ErrorOr ConvertToString(int num) => num.ToString(); -static Task> ConvertToStringAsync(int num) => Task.FromResult(ErrorOrFactory.From(num.ToString())); -static Task> ConvertToIntAsync(string str) => Task.FromResult(ErrorOrFactory.From(int.Parse(str))); +If any of the methods return an error, the chain will break and the errors will be returned. -ErrorOr errorOrString = "5"; +```cs +ErrorOr Foo() => Error.Unexpected(); -ErrorOr result = await errorOrString - .ThenAsync(str => ConvertToIntAsync(str)) - .ThenAsync(num => ConvertToStringAsync(num)) - .ThenAsync(str => ConvertToIntAsync(str)) - .ThenAsync(num => ConvertToStringAsync(num)); +ErrorOr foo = result + .Then(val => val * 2) + .Then(_ => GetAnError()) + .Then(val => $"The result is {val}") // this function will not be invoked + .Then(val => $"The result is {val}"); // this function will not be invoked +``` -// mixing `ThenAsync` and `Then` -ErrorOr result = await errorOrString - .ThenAsync(str => ConvertToIntAsync(str)) - .Then(num => ConvertToString(num)) - .ThenAsync(str => ConvertToIntAsync(str)) - .Then(num => ConvertToString(num)); +### `ThenAsync` + +`ThenAsync` receives an asynchronous action or function, and invokes it only if the result is not an error. + +```cs +ErrorOr foo = await result + .ThenAsync(val => Task.Delay(val)) + .ThenAsync(val => Task.FromResult($"The result is {val}")); ``` -If any of the methods return an error, the chain will break and the errors will be returned. +### Mixing `Then` and `ThenAsync` + +You can mix `Then` and `ThenAsync` methods together. + +```cs +ErrorOr foo = await result + .ThenAsync(val => Task.Delay(val)) + .Then(val => Console.WriteLine($"Finsihed waiting {val} seconds.")) + .ThenAsync(val => Task.FromResult(val * 2)) + .Then(val => $"The result is {val}"); +``` + +## `Else` -### `Else` / `ElseAsync` +`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. -The `Else` / `ElseAsync` methods can be used to specify a fallback value in case the state is error anywhere in the chain. +### `Else` -```csharp -// ignoring the errors -string result = errorOrString - .Then(str => ConvertToInt(str)) - .Then(num => ConvertToString(num)) +```cs +ErrorOr foo = result .Else("fallback value"); +``` -// using the errors -string result = errorOrString - .Then(str => ConvertToInt(str)) - .Then(num => ConvertToString(num)) +```cs +ErrorOr foo = result .Else(errors => $"{errors.Count} errors occurred."); ``` -```csharp -// ignoring the errors -string result = await errorOrString - .ThenAsync(str => ConvertToInt(str)) - .ThenAsync(num => ConvertToString(num)) +### `ElseAsync` + +```cs +ErrorOr foo = await result .ElseAsync(Task.FromResult("fallback value")); +``` -// using the errors -string result = await errorOrString - .ThenAsync(str => ConvertToInt(str)) - .ThenAsync(num => ConvertToString(num)) +```cs +ErrorOr foo = await result .ElseAsync(errors => Task.FromResult($"{errors.Count} errors occurred.")); ``` -## Error Types +# Mixing Features (`Then`, `Else`, `Switch`, `Match`) + +You can mix `Then`, `Else`, `Switch` and `Match` methods together. + +```cs +ErrorOr foo = await result + .ThenAsync(val => Task.Delay(val)) + .Then(val => Console.WriteLine($"Finsihed waiting {val} seconds.")) + .ThenAsync(val => Task.FromResult(val * 2)) + .Then(val => $"The result is {val}") + .Else(errors => Error.Unexpected()) + .MatchFirst( + value => value, + firstError => $"An error occurred: {firstError.Description}"); +``` + +# Error Types + +Each `Error` instance has a `Type` property, which is an enum value that represents the type of the error. -### Built-in Error Types +## Built in error types -Each error has a type out of the following options: +The following error types are built in: -```csharp +```cs public enum ErrorType { Failure, @@ -514,27 +501,32 @@ public enum ErrorType } ``` -Creating a new Error instance is done using one of the following static methods: +Each error type has a static method that creates an error of that type. For example: -```csharp -public static Error Error.Failure(string code, string description, Dictionary? metadata = null); -public static Error Error.Unexpected(string code, string description, Dictionary? metadata = null); -public static Error Error.Validation(string code, string description, Dictionary? metadata = null) -public static Error Error.Unexpected(string code, string description, Dictionary? metadata = null); -public static Error Error.Conflict(string code, string description, Dictionary? metadata = null); -public static Error Error.NotFound(string code, string description, Dictionary? metadata = null); -public static Error Error.Unauthorized(string code, string description, Dictionary? metadata = null); +```cs +var error = Error.NotFound(); ``` +optionally, you can pass a code, description and metadata to the error: + +```cs +var error = Error.Unexpected( + code: "User.ShouldNeverHappen", + description: "A user error that should never happen", + metadata: new Dictionary + { + { "user", user }, + }); +``` The `ErrorType` enum is a good way to categorize errors. -### Custom error types +## Custom error types You can create your own error types if you would like to categorize your errors differently. A custom error type can be created with the `Custom` static method -```csharp +```cs public static class MyErrorTypes { const int ShouldNeverHappen = 12; @@ -548,7 +540,7 @@ var error = Error.Custom( You can use the `Error.NumericType` method to retrieve the numeric type of the error. -```csharp +```cs var errorMessage = Error.NumericType switch { MyErrorType.ShouldNeverHappen => "Consider replacing dev team", @@ -556,17 +548,11 @@ var errorMessage = Error.NumericType switch }; ``` -### Why would I want to categorize my errors? - -If you are developing a web API, it can be useful to be able to associate the type of error that occurred to the HTTP status code that should be returned. - -If you don't want to categorize your errors, simply use the `Error.Failure` static method. - -## Built in result types +# Built in result types (`Result.Success`, ..) There are a few built in result types: -```csharp +```cs ErrorOr result = Result.Success; ErrorOr result = Result.Created; ErrorOr result = Result.Updated; @@ -575,13 +561,13 @@ ErrorOr result = Result.Deleted; Which can be used as following -```csharp +```cs ErrorOr DeleteUser(Guid id) { var user = await _userRepository.GetByIdAsync(id); if (user is null) { - return Error.NotFound(code: "User.NotFound", description: "User not found."); + return Error.NotFound(description: "User not found."); } await _userRepository.DeleteAsync(user); @@ -589,10 +575,79 @@ ErrorOr DeleteUser(Guid id) } ``` -# How Is This Different From `OneOf` or `FluentResults`? 🤔 +# Organizing Errors + +A nice approach, is creating a static class with the expected errors. For example: + +```cs +public static partial class DivisionErrors +{ + public static Error CannotDivideByZero = Error.Unexpected( + code: "Division.CannotDivideByZero", + description: "Cannot divide by zero."); +} +``` + +Which can later be used as following 👇 + +```cs +public ErrorOr Divide(int a, int b) +{ + if (b == 0) + { + return DivisionErrors.CannotDivideByZero; + } + + return a / b; +} +``` + +# [Mediator](https://github.com/jbogard/MediatR) + [FluentValidation](https://github.com/FluentValidation/FluentValidation) + `ErrorOr` 🤝 + +A common approach when using `MediatR` is to use `FluentValidation` to validate the request before it reaches the handler. + +Usually, the validation is done using a `Behavior` that throws an exception if the request is invalid. + +Using `ErrorOr`, we can create a `Behavior` that returns an error instead of throwing an exception. + +This plays nicely when the project uses `ErrorOr`, as the layer invoking the `Mediator`, similar to other components in the project, simply receives an `ErrorOr` and can handle it accordingly. + +Here is an example of a `Behavior` that validates the request and returns an error if it's invalid 👇 + +```cs +public class ValidationBehavior(IValidator? validator = null) + : IPipelineBehavior + where TRequest : IRequest + where TResponse : IErrorOr +{ + private readonly IValidator? _validator = validator; + + public async Task Handle( + TRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken) + { + if (_validator is null) + { + return await next(); + } + + var validationResult = await _validator.ValidateAsync(request, cancellationToken); + + if (validationResult.IsValid) + { + return await next(); + } + + var errors = validationResult.Errors + .ConvertAll(error => Error.Validation( + code: error.PropertyName, + description: error.ErrorMessage)); -It's similar to the others, just aims to be more intuitive and fluent. -If you find yourself typing `OneOf` or `Result.Fail("failure")` again and again, you might enjoy the fluent API of `ErrorOr` (and it's also faster). + return (dynamic)errors; + } +} +``` # Contribution 🤲 diff --git a/src/ErrorOr.ElseExtensions.cs b/src/ErrorOr.ElseExtensions.cs index b2f1536..df81abd 100644 --- a/src/ErrorOr.ElseExtensions.cs +++ b/src/ErrorOr.ElseExtensions.cs @@ -9,7 +9,7 @@ public static partial class ErrorOrExtensions /// The instance. /// The function to execute if the state is error. /// The result from calling if state is error; otherwise the original value. - public static async Task Else(this Task> errorOr, Func, TValue> onError) + public static async Task> Else(this Task> errorOr, Func, TValue> onError) { var result = await errorOr.ConfigureAwait(false); @@ -23,7 +23,7 @@ public static async Task Else(this Task> errorOr /// The instance. /// The function to execute if the state is error. /// The result from calling if state is error; otherwise the original value. - public static async Task Else(this Task> errorOr, TValue onError) + public static async Task> Else(this Task> errorOr, TValue onError) { var result = await errorOr.ConfigureAwait(false); @@ -37,7 +37,7 @@ public static async Task Else(this Task> errorOr /// The instance. /// The function to execute if the state is error. /// The result from calling if state is error; otherwise the original value. - public static async Task ElseAsync(this Task> errorOr, Func, Task> onError) + public static async Task> ElseAsync(this Task> errorOr, Func, Task> onError) { var result = await errorOr.ConfigureAwait(false); @@ -51,7 +51,91 @@ public static async Task ElseAsync(this Task> er /// The instance. /// The function to execute if the state is error. /// The result from calling if state is error; otherwise the original value. - public static async Task ElseAsync(this Task> errorOr, Task onError) + public static async Task> ElseAsync(this Task> errorOr, Task onError) + { + var result = await errorOr.ConfigureAwait(false); + + return await result.ElseAsync(onError).ConfigureAwait(false); + } + + /// + /// If the state is error, the provided function is executed asynchronously and its result is returned. + /// + /// The type of the underlying value in the . + /// The instance. + /// The function to execute if the state is error. + /// The result from calling if state is error; otherwise the original value. + public static async Task> Else(this Task> errorOr, Func, Error> onError) + { + var result = await errorOr.ConfigureAwait(false); + + return result.Else(onError); + } + + /// + /// If the state is error, the provided function is executed asynchronously and its result is returned. + /// + /// The type of the underlying value in the . + /// The instance. + /// The function to execute if the state is error. + /// The result from calling if state is error; otherwise the original value. + public static async Task> Else(this Task> errorOr, Func, List> onError) + { + var result = await errorOr.ConfigureAwait(false); + + return result.Else(onError); + } + + /// + /// If the state is error, the provided is returned. + /// + /// The type of the underlying value in the . + /// The instance. + /// The error to return. + /// The given . + public static async Task> Else(this Task> errorOr, Error error) + { + var result = await errorOr.ConfigureAwait(false); + + return result.Else(error); + } + + /// + /// If the state is error, the provided function is executed asynchronously and its result is returned. + /// + /// The type of the underlying value in the . + /// The instance. + /// The function to execute if the state is error. + /// The result from calling if state is error; otherwise the original value. + public static async Task> ElseAsync(this Task> errorOr, Func, Task> onError) + { + var result = await errorOr.ConfigureAwait(false); + + return await result.ElseAsync(onError).ConfigureAwait(false); + } + + /// + /// If the state is error, the provided function is executed asynchronously and its result is returned. + /// + /// The type of the underlying value in the . + /// The instance. + /// The function to execute if the state is error. + /// The result from calling if state is error; otherwise the original value. + public static async Task> ElseAsync(this Task> errorOr, Func, Task>> onError) + { + var result = await errorOr.ConfigureAwait(false); + + return await result.ElseAsync(onError).ConfigureAwait(false); + } + + /// + /// If the state is error, the provided function is executed asynchronously and its result is returned. + /// + /// The type of the underlying value in the . + /// The instance. + /// The function to execute if the state is error. + /// The result from calling if state is error; otherwise the original value. + public static async Task> ElseAsync(this Task> errorOr, Task onError) { var result = await errorOr.ConfigureAwait(false); diff --git a/src/ErrorOr.ToErrorOrExtensions.cs b/src/ErrorOr.ToErrorOrExtensions.cs new file mode 100644 index 0000000..c7297a9 --- /dev/null +++ b/src/ErrorOr.ToErrorOrExtensions.cs @@ -0,0 +1,28 @@ +namespace ErrorOr; + +public static partial class ErrorOrExtensions +{ + /// + /// Creates an instance with the given . + /// + public static ErrorOr ToErrorOr(this TValue value) + { + return value; + } + + /// + /// Creates an instance with the given . + /// + public static ErrorOr ToErrorOr(this Error error) + { + return error; + } + + /// + /// Creates an instance with the given . + /// + public static ErrorOr ToErrorOr(this List error) + { + return error; + } +} diff --git a/src/ErrorOr.cs b/src/ErrorOr.cs index fdbeef1..d2b764f 100644 --- a/src/ErrorOr.cs +++ b/src/ErrorOr.cs @@ -363,7 +363,52 @@ public async Task> ThenAsync(Func /// The function to execute if the state is error. /// The result from calling if state is error; otherwise the original . - public TValue Else(Func, TValue> onError) + public ErrorOr Else(Func, Error> onError) + { + if (!IsError) + { + return Value; + } + + return onError(Errors); + } + + /// + /// If the state is error, the provided function is executed and its result is returned. + /// + /// The function to execute if the state is error. + /// The result from calling if state is error; otherwise the original . + public ErrorOr Else(Func, List> onError) + { + if (!IsError) + { + return Value; + } + + return onError(Errors); + } + + /// + /// If the state is error, the provided is returned. + /// + /// The error to return. + /// The given . + public ErrorOr Else(Error error) + { + if (!IsError) + { + return Value; + } + + return error; + } + + /// + /// If the state is error, the provided function is executed and its result is returned. + /// + /// The function to execute if the state is error. + /// The result from calling if state is error; otherwise the original . + public ErrorOr Else(Func, TValue> onError) { if (!IsError) { @@ -378,7 +423,7 @@ public TValue Else(Func, TValue> onError) /// /// The value to return if the state is error. /// The result from calling if state is error; otherwise the original . - public TValue Else(TValue onError) + public ErrorOr Else(TValue onError) { if (!IsError) { @@ -393,7 +438,7 @@ public TValue Else(TValue onError) /// /// The function to execute if the state is error. /// The result from calling if state is error; otherwise the original . - public async Task ElseAsync(Func, Task> onError) + public async Task> ElseAsync(Func, Task> onError) { if (!IsError) { @@ -408,7 +453,52 @@ public async Task ElseAsync(Func, Task> onError) /// /// The function to execute if the state is error. /// The result from calling if state is error; otherwise the original . - public async Task ElseAsync(Task onError) + public async Task> ElseAsync(Func, Task> onError) + { + if (!IsError) + { + return Value; + } + + return await onError(Errors).ConfigureAwait(false); + } + + /// + /// If the state is error, the provided function is executed asynchronously and its result is returned. + /// + /// The function to execute if the state is error. + /// The result from calling if state is error; otherwise the original . + public async Task> ElseAsync(Func, Task>> onError) + { + if (!IsError) + { + return Value; + } + + return await onError(Errors).ConfigureAwait(false); + } + + /// + /// If the state is error, the provided is awaited and returned. + /// + /// The error to return if the state is error. + /// The result from awaiting the given . + public async Task> ElseAsync(Task error) + { + if (!IsError) + { + return Value; + } + + return await error.ConfigureAwait(false); + } + + /// + /// If the state is error, the provided function is executed asynchronously and its result is returned. + /// + /// The function to execute if the state is error. + /// The result from calling if state is error; otherwise the original . + public async Task> ElseAsync(Task onError) { if (!IsError) { diff --git a/src/ErrorOrFactory.cs b/src/ErrorOrFactory.cs index 62a5507..b50d213 100644 --- a/src/ErrorOrFactory.cs +++ b/src/ErrorOrFactory.cs @@ -1,18 +1,40 @@ -namespace ErrorOr; - -/// -/// Provides factory methods for creating instances of . -/// -public static class ErrorOrFactory -{ - /// - /// Creates a new instance of with a value. - /// - /// The type of the value. - /// The value to wrap. - /// An instance of containing the provided value. - public static ErrorOr From(TValue value) - { - return value; - } -} +namespace ErrorOr; + +/// +/// Provides factory methods for creating instances of . +/// +public static class ErrorOrFactory +{ + /// + /// Creates a new instance of with a value. + /// + /// The type of the value. + /// The value to wrap. + /// An instance of containing the provided value. + public static ErrorOr From(TValue value) + { + return value; + } + + /// + /// Creates a new instance of with a value. + /// + /// The type of the value. + /// The value to wrap. + /// An instance of containing the provided value. + public static ErrorOr From(Error value) + { + return value; + } + + /// + /// Creates a new instance of with a value. + /// + /// The type of the value. + /// The value to wrap. + /// An instance of containing the provided value. + public static ErrorOr From(List value) + { + return value; + } +} diff --git a/tests/ErrorOr.ElseAsyncTests.cs b/tests/ErrorOr.ElseAsyncTests.cs index 9ca2a21..2d8d5b9 100644 --- a/tests/ErrorOr.ElseAsyncTests.cs +++ b/tests/ErrorOr.ElseAsyncTests.cs @@ -6,67 +6,173 @@ namespace Tests; public class ElseAsyncTests { [Fact] - public async Task CallingElseAsync_WhenIsSuccess_ShouldNotInvokeElseFunc() + public async Task CallingElseAsyncWithValueFunc_WhenIsSuccess_ShouldNotInvokeElseFunc() { // Arrange ErrorOr errorOrString = "5"; // Act - string result = await errorOrString + ErrorOr result = await errorOrString .ThenAsync(str => ConvertToIntAsync(str)) .ThenAsync(num => ConvertToStringAsync(num)) .ElseAsync(errors => Task.FromResult($"Error count: {errors.Count}")); // Assert - result.Should().BeEquivalentTo(errorOrString.Value); + result.IsError.Should().BeFalse(); + result.Value.Should().BeEquivalentTo(errorOrString.Value); } [Fact] - public async Task CallingElseAsync_WhenIsError_ShouldInvokeElseFunc() + public async Task CallingElseAsyncWithValueFunc_WhenIsError_ShouldInvokeElseFunc() { // Arrange ErrorOr errorOrString = Error.NotFound(); // Act - string result = await errorOrString + var result = await errorOrString .ThenAsync(str => ConvertToIntAsync(str)) .ThenAsync(num => ConvertToStringAsync(num)) .ElseAsync(errors => Task.FromResult($"Error count: {errors.Count}")); // Assert - result.Should().BeEquivalentTo("Error count: 1"); + result.IsError.Should().BeFalse(); + result.Value.Should().BeEquivalentTo("Error count: 1"); } [Fact] - public async Task CallingElseAsync_WhenIsSuccess_ShouldNotReturnElseValue() + public async Task CallingElseAsyncWithValue_WhenIsSuccess_ShouldNotReturnElseValue() { // Arrange ErrorOr errorOrString = "5"; // Act - string result = await errorOrString + var result = await errorOrString .ThenAsync(str => ConvertToIntAsync(str)) .ThenAsync(num => ConvertToStringAsync(num)) .ElseAsync(Task.FromResult("oh no")); // Assert - result.Should().BeEquivalentTo(errorOrString.Value); + result.IsError.Should().BeFalse(); + result.Value.Should().BeEquivalentTo(errorOrString.Value); } [Fact] - public async Task CallingElseAsync_WhenIsError_ShouldReturnElseValue() + public async Task CallingElseAsyncWithValue_WhenIsError_ShouldReturnElseValue() { // Arrange ErrorOr errorOrString = Error.NotFound(); // Act - string result = await errorOrString + var result = await errorOrString .ThenAsync(str => ConvertToIntAsync(str)) .ThenAsync(num => ConvertToStringAsync(num)) .ElseAsync(Task.FromResult("oh no")); // Assert - result.Should().BeEquivalentTo("oh no"); + result.IsError.Should().BeFalse(); + result.Value.Should().BeEquivalentTo("oh no"); + } + + [Fact] + public async Task CallingElseAsyncWithError_WhenIsError_ShouldReturnElseError() + { + // Arrange + ErrorOr errorOrString = Error.NotFound(); + + // Act + var result = await errorOrString + .ThenAsync(str => ConvertToIntAsync(str)) + .ThenAsync(num => ConvertToStringAsync(num)) + .ElseAsync(Task.FromResult(Error.Unexpected())); + + // Assert + result.IsError.Should().BeTrue(); + result.FirstError.Type.Should().Be(ErrorType.Unexpected); + } + + [Fact] + public async Task CallingElseAsyncWithError_WhenIsSuccess_ShouldNotReturnElseError() + { + // Arrange + ErrorOr errorOrString = "5"; + + // Act + var result = await errorOrString + .ThenAsync(str => ConvertToIntAsync(str)) + .ThenAsync(num => ConvertToStringAsync(num)) + .ElseAsync(Task.FromResult(Error.Unexpected())); + + // Assert + result.IsError.Should().BeFalse(); + result.Value.Should().Be(errorOrString.Value); + } + + [Fact] + public async Task CallingElseAsyncWithErrorFunc_WhenIsError_ShouldReturnElseError() + { + // Arrange + ErrorOr errorOrString = Error.NotFound(); + + // Act + var result = await errorOrString + .ThenAsync(str => ConvertToIntAsync(str)) + .ThenAsync(num => ConvertToStringAsync(num)) + .ElseAsync(errors => Task.FromResult(Error.Unexpected())); + + // Assert + result.IsError.Should().BeTrue(); + result.FirstError.Type.Should().Be(ErrorType.Unexpected); + } + + [Fact] + public async Task CallingElseAsyncWithErrorFunc_WhenIsSuccess_ShouldNotReturnElseError() + { + // Arrange + ErrorOr errorOrString = "5"; + + // Act + var result = await errorOrString + .ThenAsync(str => ConvertToIntAsync(str)) + .ThenAsync(num => ConvertToStringAsync(num)) + .ElseAsync(Task.FromResult(Error.Unexpected())); + + // Assert + result.IsError.Should().BeFalse(); + result.Value.Should().Be(errorOrString.Value); + } + + [Fact] + public async Task CallingElseAsyncWithErrorFunc_WhenIsError_ShouldReturnElseErrors() + { + // Arrange + ErrorOr errorOrString = Error.NotFound(); + + // Act + var result = await errorOrString + .ThenAsync(str => ConvertToIntAsync(str)) + .ThenAsync(num => ConvertToStringAsync(num)) + .ElseAsync(errors => Task.FromResult(new List { Error.Unexpected() })); + + // Assert + result.IsError.Should().BeTrue(); + result.FirstError.Type.Should().Be(ErrorType.Unexpected); + } + + [Fact] + public async Task CallingElseAsyncWithErrorFunc_WhenIsSuccess_ShouldNotReturnElseErrors() + { + // Arrange + ErrorOr errorOrString = "5"; + + // Act + var result = await errorOrString + .ThenAsync(str => ConvertToIntAsync(str)) + .ThenAsync(num => ConvertToStringAsync(num)) + .ElseAsync(errors => Task.FromResult(new List { Error.Unexpected() })); + + // Assert + result.IsError.Should().BeFalse(); + result.Value.Should().Be(errorOrString.Value); } private static Task> ConvertToStringAsync(int num) => Task.FromResult(ErrorOrFactory.From(num.ToString())); diff --git a/tests/ErrorOr.ElseTests.cs b/tests/ErrorOr.ElseTests.cs index 48fb642..89327bd 100644 --- a/tests/ErrorOr.ElseTests.cs +++ b/tests/ErrorOr.ElseTests.cs @@ -6,99 +6,207 @@ namespace Tests; public class ElseTests { [Fact] - public void CallingElse_WhenIsSuccess_ShouldNotInvokeElseFunc() + public void CallingElseWithValueFunc_WhenIsSuccess_ShouldNotInvokeElseFunc() { // Arrange ErrorOr errorOrString = "5"; // Act - string result = errorOrString + ErrorOr result = errorOrString .Then(str => ConvertToInt(str)) .Then(num => ConvertToString(num)) .Else(errors => $"Error count: {errors.Count}"); // Assert - result.Should().BeEquivalentTo(errorOrString.Value); + result.IsError.Should().BeFalse(); + result.Value.Should().BeEquivalentTo(errorOrString.Value); } [Fact] - public void CallingElse_WhenIsError_ShouldInvokeElseFunc() + public void CallingElseWithValueFunc_WhenIsError_ShouldReturnElseValue() { // Arrange ErrorOr errorOrString = Error.NotFound(); // Act - string result = errorOrString + ErrorOr result = errorOrString .Then(str => ConvertToInt(str)) .Then(num => ConvertToString(num)) .Else(errors => $"Error count: {errors.Count}"); // Assert - result.Should().BeEquivalentTo("Error count: 1"); + result.IsError.Should().BeFalse(); + result.Value.Should().BeEquivalentTo("Error count: 1"); } [Fact] - public void CallingElse_WhenIsSuccess_ShouldNotReturnElseValue() + public void CallingElseWithValue_WhenIsSuccess_ShouldNotReturnElseValue() { // Arrange ErrorOr errorOrString = "5"; // Act - string result = errorOrString + ErrorOr result = errorOrString .Then(str => ConvertToInt(str)) .Then(num => ConvertToString(num)) .Else("oh no"); // Assert - result.Should().BeEquivalentTo(errorOrString.Value); + result.IsError.Should().BeFalse(); + result.Value.Should().BeEquivalentTo(errorOrString.Value); } [Fact] - public void CallingElse_WhenIsError_ShouldReturnElseValue() + public void CallingElseWithValue_WhenIsError_ShouldReturnElseValue() { // Arrange ErrorOr errorOrString = Error.NotFound(); // Act - string result = errorOrString + ErrorOr result = errorOrString .Then(str => ConvertToInt(str)) .Then(num => ConvertToString(num)) .Else("oh no"); // Assert - result.Should().BeEquivalentTo("oh no"); + result.IsError.Should().BeFalse(); + result.Value.Should().BeEquivalentTo("oh no"); } [Fact] - public async Task CallingElseAfterThenAsync_WhenIsError_ShouldReturnElseValue() + public void CallingElseWithError_WhenIsError_ShouldReturnElseError() { // Arrange ErrorOr errorOrString = Error.NotFound(); // Act - string result = await errorOrString + ErrorOr result = errorOrString + .Then(str => ConvertToInt(str)) + .Then(num => ConvertToString(num)) + .Else(Error.Unexpected()); + + // Assert + result.IsError.Should().BeTrue(); + result.FirstError.Type.Should().Be(ErrorType.Unexpected); + } + + [Fact] + public void CallingElseWithErrorsFunc_WhenIsError_ShouldReturnElseError() + { + // Arrange + ErrorOr errorOrString = Error.NotFound(); + + // Act + ErrorOr result = errorOrString + .Then(str => ConvertToInt(str)) + .Then(num => ConvertToString(num)) + .Else(errors => Error.Unexpected()); + + // Assert + result.IsError.Should().BeTrue(); + result.FirstError.Type.Should().Be(ErrorType.Unexpected); + } + + [Fact] + public void CallingElseWithErrorsFunc_WhenIsError_ShouldReturnElseErrors() + { + // Arrange + ErrorOr errorOrString = Error.NotFound(); + + // Act + ErrorOr result = errorOrString + .Then(str => ConvertToInt(str)) + .Then(num => ConvertToString(num)) + .Else(errors => new() { Error.Unexpected() }); + + // Assert + result.IsError.Should().BeTrue(); + result.FirstError.Type.Should().Be(ErrorType.Unexpected); + } + + [Fact] + public async Task CallingElseWithValueAfterThenAsync_WhenIsError_ShouldReturnElseValue() + { + // Arrange + ErrorOr errorOrString = Error.NotFound(); + + // Act + ErrorOr result = await errorOrString .Then(str => ConvertToInt(str)) .ThenAsync(num => ConvertToStringAsync(num)) .Else("oh no"); // Assert - result.Should().BeEquivalentTo("oh no"); + result.IsError.Should().BeFalse(); + result.Value.Should().BeEquivalentTo("oh no"); } [Fact] - public async Task CallingElseAfterThenAsync_WhenIsError_ShouldInvokeElseFunc() + public async Task CallingElseWithValueFuncAfterThenAsync_WhenIsError_ShouldReturnElseValue() { // Arrange ErrorOr errorOrString = Error.NotFound(); // Act - string result = await errorOrString + ErrorOr result = await errorOrString .Then(str => ConvertToInt(str)) .ThenAsync(num => ConvertToStringAsync(num)) .Else(errors => $"Error count: {errors.Count}"); // Assert - result.Should().BeEquivalentTo("Error count: 1"); + result.IsError.Should().BeFalse(); + result.Value.Should().BeEquivalentTo("Error count: 1"); + } + + [Fact] + public async Task CallingElseWithErrorAfterThenAsync_WhenIsError_ShouldReturnElseError() + { + // Arrange + ErrorOr errorOrString = Error.NotFound(); + + // Act + ErrorOr result = await errorOrString + .Then(str => ConvertToInt(str)) + .ThenAsync(num => ConvertToStringAsync(num)) + .Else(Error.Unexpected()); + + // Assert + result.IsError.Should().BeTrue(); + result.FirstError.Type.Should().Be(ErrorType.Unexpected); + } + + [Fact] + public async Task CallingElseWithErrorFuncAfterThenAsync_WhenIsError_ShouldReturnElseError() + { + // Arrange + ErrorOr errorOrString = Error.NotFound(); + + // Act + ErrorOr result = await errorOrString + .Then(str => ConvertToInt(str)) + .ThenAsync(num => ConvertToStringAsync(num)) + .Else(errors => Error.Unexpected()); + + // Assert + result.IsError.Should().BeTrue(); + result.FirstError.Type.Should().Be(ErrorType.Unexpected); + } + + [Fact] + public async Task CallingElseWithErrorFuncAfterThenAsync_WhenIsError_ShouldReturnElseErrors() + { + // Arrange + ErrorOr errorOrString = Error.NotFound(); + + // Act + ErrorOr result = await errorOrString + .Then(str => ConvertToInt(str)) + .ThenAsync(num => ConvertToStringAsync(num)) + .Else(errors => new() { Error.Unexpected() }); + + // Assert + result.IsError.Should().BeTrue(); + result.FirstError.Type.Should().Be(ErrorType.Unexpected); } private static ErrorOr ConvertToString(int num) => num.ToString(); diff --git a/tests/ErrorOr.ToErrorOrTests.cs b/tests/ErrorOr.ToErrorOrTests.cs new file mode 100644 index 0000000..47ec243 --- /dev/null +++ b/tests/ErrorOr.ToErrorOrTests.cs @@ -0,0 +1,49 @@ +using ErrorOr; +using FluentAssertions; + +namespace Tests; + +public class ToErrorOrTests +{ + [Fact] + public void ValueToErrorOr_WhenAccessingValue_ShouldReturnValue() + { + // Arrange + var value = 5; + + // Act + var result = value.ToErrorOr(); + + // Assert + result.IsError.Should().BeFalse(); + result.Value.Should().Be(value); + } + + [Fact] + public void ErrorToErrorOr_WhenAccessingFirstError_ShouldReturnSameError() + { + // Arrange + var error = Error.Unauthorized(); + + // Act + var result = error.ToErrorOr(); + + // Assert + result.IsError.Should().BeTrue(); + result.FirstError.Should().Be(error); + } + + [Fact] + public void ListOfErrorsToErrorOr_WhenAccessingErrors_ShouldReturnSameErrors() + { + // Arrange + var errors = new List { Error.Unauthorized(), Error.Validation() }; + + // Act + var result = errors.ToErrorOr(); + + // Assert + result.IsError.Should().BeTrue(); + result.Errors.Should().BeEquivalentTo(errors); + } +}