ErrorOr 2.0.1
dotnet add package ErrorOr --version 2.0.1
NuGet\Install-Package ErrorOr -Version 2.0.1
<PackageReference Include="ErrorOr" Version="2.0.1" />
<PackageVersion Include="ErrorOr" Version="2.0.1" />
<PackageReference Include="ErrorOr" />
paket add ErrorOr --version 2.0.1
#r "nuget: ErrorOr, 2.0.1"
#:package [email protected]
#addin nuget:?package=ErrorOr&version=2.0.1
#tool nuget:?package=ErrorOr&version=2.0.1
<div align="center">
<img src="assets/icon.png" alt="drawing" width="700px"/></br>
A simple, fluent discriminated union of an error or a result.
dotnet add package ErrorOr
</div>
- Give it a star ⭐!
- Getting Started 🏃
- Creating an
ErrorOrinstance - Properties
- Methods
- Mixing Features (
Then,FailIf,Else,Switch,Match) - Error Types
- Built in result types (
Result.Success, ..) - Organizing Errors
- Mediator + FluentValidation +
ErrorOr🤝 - Contribution 🤲
- Credits 🙏
- License 🪪
Give it a star ⭐!
Loving it? Show your support by giving this project a star!
Getting Started 🏃
Replace throwing exceptions with ErrorOr<T>
This 👇
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);
Console.WriteLine(result * 2); // 4
}
catch (Exception e)
{
Console.WriteLine(e.Message);
return;
}
Turns into this 👇
public ErrorOr<float> Divide(int a, int b)
{
if (b == 0)
{
return Error.Unexpected(description: "Cannot divide by zero");
}
return a / b;
}
var result = Divide(4, 2);
if (result.IsError)
{
Console.WriteLine(result.FirstError.Description);
return;
}
Console.WriteLine(result.Value * 2); // 4
Or, using Then/Else and Switch/Match, you can do this 👇
Divide(4, 2)
.Then(val => val * 2)
.SwitchFirst(
onValue: Console.WriteLine, // 4
onFirstError: error => Console.WriteLine(error.Description));
Support For Multiple Errors
Internally, the ErrorOr object has a list of Errors, so if you have multiple errors, you don't need to compromise and have only the first one.
public class User(string _name)
{
public static ErrorOr<User> Create(string name)
{
List<Error> errors = [];
if (name.Length < 2)
{
errors.Add(Error.Validation(description: "Name is too short"));
}
if (name.Length > 100)
{
errors.Add(Error.Validation(description: "Name is too long"));
}
if (string.IsNullOrWhiteSpace(name))
{
errors.Add(Error.Validation(description: "Name cannot be empty or whitespace only"));
}
if (errors.Count > 0)
{
return errors;
}
return new User(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
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
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
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: Yikes
Creating an ErrorOr instance
Using implicit conversion
There are implicit converters from TResult, Error, List<Error> to ErrorOr<TResult>
ErrorOr<int> result = 5;
ErrorOr<int> result = Error.Unexpected();
ErrorOr<int> result = [Error.Validation(), Error.Validation()];
public ErrorOr<int> IntToErrorOr()
{
return 5;
}
public ErrorOr<int> SingleErrorToErrorOr()
{
return Error.Unexpected();
}
public ErrorOr<int> MultipleErrorsToErrorOr()
{
return [
Error.Validation(description: "Invalid Name"),
Error.Validation(description: "Invalid Last Name")
];
}
Using The ErrorOrFactory
ErrorOr<int> result = ErrorOrFactory.From(5);
ErrorOr<int> result = ErrorOrFactory.From<int>(Error.Unexpected());
ErrorOr<int> result = ErrorOrFactory.From<int>([Error.Validation(), Error.Validation()]);
public ErrorOr<int> GetValue()
{
return ErrorOrFactory.From(5);
}
public ErrorOr<int> SingleErrorToErrorOr()
{
return ErrorOrFactory.From<int>(Error.Unexpected());
}
public ErrorOr<int> MultipleErrorsToErrorOr()
{
return ErrorOrFactory.From([
Error.Validation(description: "Invalid Name"),
Error.Validation(description: "Invalid Last Name")
]);
}
Using The ToErrorOr Extension Method
ErrorOr<int> result = 5.ToErrorOr();
ErrorOr<int> result = Error.Unexpected().ToErrorOr<int>();
ErrorOr<int> result = new[] { Error.Validation(), Error.Validation() }.ToErrorOr<int>();
Properties
IsError
ErrorOr<int> result = User.Create();
if (result.IsError)
{
// the result contains one or more errors
}
Value
ErrorOr<int> result = User.Create();
if (!result.IsError) // the result contains a value
{
Console.WriteLine(result.Value);
}
Errors
ErrorOr<int> result = User.Create();
if (result.IsError)
{
result.Errors // contains the list of errors that occurred
.ForEach(error => Console.WriteLine(error.Description));
}
FirstError
ErrorOr<int> result = User.Create();
if (result.IsError)
{
var firstError = result.FirstError; // only the first error that occurred
Console.WriteLine(firstError == result.Errors[0]); // true
}
ErrorsOrEmptyList
ErrorOr<int> result = User.Create();
if (result.IsError)
{
result.ErrorsOrEmptyList // List<Error> { /* one or more errors */ }
return;
}
result.ErrorsOrEmptyList // List<Error> { }
Methods
Match
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
string foo = result.Match(
value => value,
errors => $"{errors.Count} errors occurred.");
MatchAsync
string foo = await result.MatchAsync(
value => Task.FromResult(value),
errors => Task.FromResult($"{errors.Count} errors occurred."));
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.
string foo = result.MatchFirst(
value => value,
firstError => firstError.Description);
MatchFirstAsync
string foo = await result.MatchFirstAsync(
value => Task.FromResult(value),
firstError => Task.FromResult(firstError.Description));
Switch
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.
Switch
result.Switch(
value => Console.WriteLine(value),
errors => Console.WriteLine($"{errors.Count} errors occurred."));
SwitchAsync
await result.SwitchAsync(
value => { Console.WriteLine(value); return Task.CompletedTask; },
errors => { Console.WriteLine($"{errors.Count} errors occurred."); return Task.CompletedTask; });
SwitchFirst
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.
Unlike Switch, if the state is error, SwitchFirst's onError function receives only the first error that occurred, not the entire list of errors.
result.SwitchFirst(
value => Console.WriteLine(value),
firstError => Console.WriteLine(firstError.Description));
SwitchFirstAsync
await result.SwitchFirstAsync(
value => { Console.WriteLine(value); return Task.CompletedTask; },
firstError => { Console.WriteLine(firstError.Description); return Task.CompletedTask; });
Then
Then
Then receives a function, and invokes it only if the result is not an error.
ErrorOr<int> foo = result
.Then(val => val * 2);
Multiple Then methods can be chained together.
ErrorOr<string> foo = result
.Then(val => val * 2)
.Then(val => $"The result is {val}");
If any of the methods return an error, the chain will break and the errors will be returned.
ErrorOr<int> Foo() => Error.Unexpected();
ErrorOr<string> 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
ThenAsync
ThenAsync receives an asynchronous function, and invokes it only if the result is not an error.
ErrorOr<string> foo = await result
.ThenAsync(val => DoSomethingAsync(val))
.ThenAsync(val => DoSomethingElseAsync($"The result is {val}"));
ThenDo and ThenDoAsync
ThenDo and ThenDoAsync are similar to Then and ThenAsync, but instead of invoking a function that returns a value, they invoke an action.
ErrorOr<string> foo = result
.ThenDo(val => Console.WriteLine(val))
.ThenDo(val => Console.WriteLine($"The result is {val}"));
ErrorOr<string> foo = await result
.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.
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.
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.
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.
Else
ErrorOr<string> foo = result
.Else("fallback value");
ErrorOr<string> foo = result
.Else(errors => $"{errors.Count} errors occurred.");
ElseAsync
ErrorOr<string> foo = await result
.ElseAsync(Task.FromResult("fallback value"));
ErrorOr<string> foo = await result
.ElseAsync(errors => Task.FromResult($"{errors.Count} errors occurred."));
Mixing Features (Then, FailIf, Else, Switch, Match)
You can mix Then, FailIf, Else, Switch and Match methods together.
ErrorOr<string> foo = await result
.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())
.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
The following error types are built in:
public enum ErrorType
{
Failure,
Unexpected,
Validation,
Conflict,
NotFound,
Unauthorized,
Forbidden,
}
Each error type has a static method that creates an error of that type. For example:
var error = Error.NotFound();
optionally, you can pass a code, description and metadata to the error:
var error = Error.Unexpected(
code: "User.ShouldNeverHappen",
description: "A user error that should never happen",
metadata: new Dictionary<string, object>
{
{ "user", user },
});
The ErrorType enum is a good way to categorize errors.
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
public static class MyErrorTypes
{
const int ShouldNeverHappen = 12;
}
var error = Error.Custom(
type: MyErrorTypes.ShouldNeverHappen,
code: "User.ShouldNeverHappen",
description: "A user error that should never happen");
You can use the Error.NumericType method to retrieve the numeric type of the error.
var errorMessage = Error.NumericType switch
{
MyErrorType.ShouldNeverHappen => "Consider replacing dev team",
_ => "An unknown error occurred.",
};
Built in result types (Result.Success, ..)
There are a few built in result types:
ErrorOr<Success> result = Result.Success;
ErrorOr<Created> result = Result.Created;
ErrorOr<Updated> result = Result.Updated;
ErrorOr<Deleted> result = Result.Deleted;
Which can be used as following
ErrorOr<Deleted> DeleteUser(Guid id)
{
var user = await _userRepository.GetByIdAsync(id);
if (user is null)
{
return Error.NotFound(description: "User not found.");
}
await _userRepository.DeleteAsync(user);
return Result.Deleted;
}
Organizing Errors
A nice approach, is creating a static class with the expected errors. For example:
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 👇
public ErrorOr<float> Divide(int a, int b)
{
if (b == 0)
{
return DivisionErrors.CannotDivideByZero;
}
return a / b;
}
Mediator + 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 👇
public class ValidationBehavior<TRequest, TResponse>(IValidator<TRequest>? validator = null)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
where TResponse : IErrorOr
{
private readonly IValidator<TRequest>? _validator = validator;
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> 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;
}
}
Contribution 🤲
If you have any questions, comments, or suggestions, please open an issue or create a pull request 🙂
Credits 🙏
- OneOf - An awesome library which provides F# style discriminated unions behavior for C#
License 🪪
This project is licensed under the terms of the MIT license.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- No dependencies.
-
net6.0
- No dependencies.
NuGet packages (55)
Showing the top 5 NuGet packages that depend on ErrorOr:
| Package | Downloads |
|---|---|
|
GymManager.Common
A agrouped common entities for GymManager |
|
|
FEE.Remit.Generic.Endpoint
Endpoint Helper |
|
|
ErrorOrAspNetCoreExtensions
A collection of ErrorOr extension methods designed to reduce the amount of boilerplate code needed when returning appropriate HTTP responses. |
|
|
ErrorOr.Extensions
Package Description |
|
|
RealmDigital.Common
Common classes and interfaces used when building .NET 6+ applications |
GitHub repositories (10)
Showing the top 10 popular GitHub repositories that depend on ErrorOr:
| Repository | Stars |
|---|---|
|
evolutionary-architecture/evolutionary-architecture-by-example
Navigate the complex landscape of .NET software architecture with our step-by-step, story-like guide. Unpack the interplay between modular monoliths, microservices, domain-driven design, and various architectural patterns. Go beyond the one-size-fits-all solutions and understand how to blend these approaches based on your unique needs.
|
|
|
amantinband/clean-architecture
The ultimate clean architecture template for .NET applications 💪
|
|
|
nadirbad/VerticalSliceArchitecture
Vertical Slice Architecture solution template in .NET 9
|
|
|
amantinband/buber-breakfast
A REST API which supports Creating, Reading, Updating and Deleting breakfasts
|
|
|
Jorixon/JASM
Just Another Skin Manager
|
|
|
SSWConsulting/SSW.VerticalSliceArchitecture
An enterprise ready solution template for Vertical Slice Architecture. This template is just one way to apply the Vertical Slice Architecture.
|
|
|
SSWConsulting/SSW.CleanArchitecture
SSW Clean Architecture Template
|
|
|
asesidaa/EXVS2-POC
|
|
|
mohammadKarimi/SwiftLink
SwiftLink is a modern URL shortener with Asp.net Core 8
|
|
|
PanKunik/buber-dinner
This repository follows Amichai Mantinband course on REST API DDD CLEAN ARCHITECTURE.
|
| Version | Downloads | Last Updated |
|---|---|---|
| 2.0.1 | 2,660,557 | 3/26/2024 |
| 1.10.0 | 358,851 | 2/14/2024 |
| 1.9.0 | 225,228 | 1/6/2024 |
| 1.8.0 | 1,944 | 1/5/2024 |
| 1.7.0 | 544 | 1/5/2024 |
| 1.6.0 | 32,554 | 1/4/2024 |
| 1.5.0 | 8,571 | 1/2/2024 |
| 1.4.0 | 911 | 1/1/2024 |
| 1.3.0 | 699,132 | 10/1/2023 |
| 1.2.1 | 1,145,460 | 11/24/2022 |
| 1.2.0 | 11,054 | 11/17/2022 |
| 1.1.0 | 9,677 | 11/14/2022 |
| 1.0.1 | 88,565 | 9/22/2022 |
| 1.0.0 | 25,324 | 8/14/2022 |
| 0.2.0 | 6,611 | 7/31/2022 |
| 0.1.0 | 12,749 | 7/4/2022 |
| 0.0.5 | 1,221 | 6/1/2022 |
| 0.0.2 | 861 | 5/31/2022 |