- Give it a star ⭐!
- Getting Started 🏃
- Creating an
ErrorOr
instance - Properties
- Methods
- Mixing Features (
Then
,Else
,Switch
,Match
) - Error Types
- Built in result types (
Result.Success
, ..) - Organizing Errors
- Mediator + FluentValidation +
ErrorOr
🤝 - Contribution 🤲
- Credits 🙏
- License 🪪
Loving it? Show your support by giving this project a star!
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));
ErrorOr<string> foo = await "2".ToErrorOr()
.Then(int.Parse) // 2
.When(val => val > 2, Error.Validation(description: $"{val} is too big") // 2
.ThenAsync(Task.Delay) // Sleep for 2 milliseconds
.Then(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}");
ErrorOr<string> foo = await "5".ToErrorOr()
.Then(int.Parse) // 5
.When(val => val > 2, Error.Validation(description: $"{val} is too big") // 2
.ThenAsync(Task.Delay) // Error.Validation()
.Then(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
var result = await "-1".ToErrorOr()
.Then(int.Parse) // -1
.Where(val => val < 2, Error.Validation(description: $"{val} is too big") // -1
.Else(Error.Unexpected("Big")))) // -1
.Where(val => val > 0, Error.Validation(description: $"{val} is too small")) // Error.Validation()
.Else(Error.Unexpected("Small"))); // Error.Unexpected()
try
{
var user = await _userRepository.GetByIdAsync(id) // throws an exception if something goes wrong
?? throw new NotFoundException("User not found");
user.IncrementAge(); // throws UserTooOldException if age is over 130, but may also throw other exceptions
if (!user.IsOverAge(18)) throw new ValidationException("User is under age");
await _userRepository.UpdateAsync(incrementAgeResult.Value); // throws an exception if something goes wrong
return NoContent();
}
catch (Exception e)
{
return e switch
{
NotFoundException => ..,
ValidationException => ..,
UserTooOldException => ..,
_ => InternalServerError(),
};
}
ErrorOr<User> user = await _userRepository.GetByIdAsync(id);
if (user.IsError)
{
return user.Errors.ToActionResult();
}
var incrementAgeResult = user.Value.IncrementAge();
if (incrementAgeResult.IsError)
{
return incrementAgeResult.Errors.ToActionResult();
}
if (!incrementAgeResult.Value.IsOverAge(18))
{
return UserErrors.UnderAge.ToActionResult();
}
var updateUserResult = await _userRepository.UpdateAsync(incrementAgeResult.Value);
if (updateUserResult.IsError)
{
return updateUserResult.Errors.ToActionResult();
}
return NoContent();
return await _userRepository.GetByIdAsync(id)
.Then(user => user.IncrementAge()
.Then(success => user)
.Else(errors => Error.Unexpected("Not expected to fail")))
.When(user => !user.IsOverAge(18), UserErrors.UnderAge)
.ThenDo(user => _logger.LogInformation($"User {user.Id} incremented age to {user.Age}"))
.ThenAsync(user => _userRepository.UpdateAsync(user))
.Finally(
_ => NoContent(),
errors => errors.ToActionResult());
return await _userRepository.GetByIdAsync(id)
.Then(user => user.IncrementAge().Then(success => user))
.When(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());
var hiResult = user.SayHi();
if (hiResult.IsError)
{
return Error.Validation("bad hi");
}
var byeResult = user.SayBye();
if (byeResult.IsError)
{
return Error.Validation("bad bye");
}
return Result.Success
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);
}
}
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")
];
}
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")
]);
}
ErrorOr<int> result = 5.ToErrorOr();
ErrorOr<int> result = Error.Unexpected().ToErrorOr<int>();
ErrorOr<int> result = new[] { Error.Validation(), Error.Validation() }.ToErrorOr<int>();
ErrorOr<int> result = User.Create();
if (result.IsError)
{
// the result contains one or more errors
}
ErrorOr<int> result = User.Create();
if (!result.IsError) // the result contains a value
{
Console.WriteLine(result.Value);
}
ErrorOr<int> result = User.Create();
if (result.IsError)
{
result.Errors // contains the list of errors that occurred
.ForEach(error => Console.WriteLine(error.Description));
}
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
}
ErrorOr<int> result = User.Create();
if (result.IsError)
{
result.ErrorsOrEmptyList // List<Error> { /* one or more errors */ }
return;
}
result.ErrorsOrEmptyList // List<Error> { }
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.
string foo = result.Match(
value => value,
errors => $"{errors.Count} errors occurred.");
string foo = await result.MatchAsync(
value => Task.FromResult(value),
errors => Task.FromResult($"{errors.Count} errors occurred."));
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);
string foo = await result.MatchFirstAsync(
value => Task.FromResult(value),
firstError => Task.FromResult(firstError.Description));
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.
result.Switch(
value => Console.WriteLine(value),
errors => Console.WriteLine($"{errors.Count} errors occurred."));
await result.SwitchAsync(
value => { Console.WriteLine(value); return Task.CompletedTask; },
errors => { Console.WriteLine($"{errors.Count} errors occurred."); return Task.CompletedTask; });
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));
await result.SwitchFirstAsync(
value => { Console.WriteLine(value); return Task.CompletedTask; },
firstError => { Console.WriteLine(firstError.Description); return Task.CompletedTask; });
Then
receives an action or 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
receives an asynchronous action or function, and invokes it only if the result is not an error.
ErrorOr<string> foo = await result
.ThenAsync(val => Task.Delay(val))
.ThenAsync(val => Task.FromResult($"The result is {val}"));
You can mix Then
and ThenAsync
methods together.
ErrorOr<string> 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
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.
ErrorOr<string> foo = result
.Else("fallback value");
ErrorOr<string> foo = result
.Else(errors => $"{errors.Count} errors occurred.");
ErrorOr<string> foo = await result
.ElseAsync(Task.FromResult("fallback value"));
ErrorOr<string> foo = await result
.ElseAsync(errors => Task.FromResult($"{errors.Count} errors occurred."));
You can mix Then
, Else
, Switch
and Match
methods together.
ErrorOr<string> 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}");
Each Error
instance has a Type
property, which is an enum value that represents the type of the error.
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.
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.",
};
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;
}
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;
}
}
If you have any questions, comments, or suggestions, please open an issue or create a pull request 🙂
- OneOf - An awesome library which provides F# style discriminated unions behavior for C#
This project is licensed under the terms of the MIT license.