Forge.ErrorOr
2.0.1
dotnet add package Forge.ErrorOr --version 2.0.1
NuGet\Install-Package Forge.ErrorOr -Version 2.0.1
<PackageReference Include="Forge.ErrorOr" Version="2.0.1" />
paket add Forge.ErrorOr --version 2.0.1
#r "nuget: Forge.ErrorOr, 2.0.1"
// Install Forge.ErrorOr as a Cake Addin #addin nuget:?package=Forge.ErrorOr&version=2.0.1 // Install Forge.ErrorOr as a Cake Tool #tool nuget:?package=Forge.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
ErrorOr
instance - 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 Error
s, 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 is compatible. 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 is compatible. 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. |
.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
- Microsoft.Bcl.HashCode (>= 1.1.1)
-
net6.0
- No dependencies.
-
net7.0
- No dependencies.
-
net8.0
- No dependencies.
NuGet packages (3)
Showing the top 3 NuGet packages that depend on Forge.ErrorOr:
Package | Downloads |
---|---|
Sesame.Web.Clients.Registry.Shared
Sesame Web Clients Registry Shared |
|
Sesame.Web.Clients.Gateway
Client for Sesame gateway communication |
|
Sesame.DataHub.Contracts
Sesame DataHub Contracts |
GitHub repositories
This package is not used by any popular GitHub repositories.