- Give it a star โญ!
- Getting Started
- A more practical example
- Dropping the exceptions throwing logic
- Usage
- How Is This Different From
OneOf<T0, T1>
orFluentResults
? - Contribution
- Credits
- License
Give it a star โญ!
Loving it? Show your support by giving this project a star!
Getting Started
Single Error
This ๐๐ฝ
User GetUser(Guid id = default)
{
if (id == default)
{
throw new ValidationException("Id is required");
}
return new User(Name: "Amichai");
}
try
{
var user = GetUser();
Console.WriteLine(user.Name);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
Turns into this ๐๐ฝ
ErrorOr<User> GetUser(Guid id = default)
{
if (id == default)
{
return Error.Validation("Id is required");
}
return new User(Name: "Amichai");
}
errorOrUser.SwitchFirst(
user => Console.WriteLine(user.Name),
error => Console.WriteLine(error.Description));
This ๐๐ฝ
void AddUser(User user)
{
if (!_users.TryAdd(user))
{
throw new Exception("Failed to add user");
}
}
Turns into this ๐๐ฝ
ErrorOr<Created> AddUser(User user)
{
if (!_users.TryAdd(user))
{
return Error.Failure(description: "Failed to add user");
}
return Result.Created;
}
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
{
public string Name { get; }
private User(string name)
{
Name = name;
}
public static ErrorOr<User> Create(string name)
{
List<Error> errors = new();
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(firstName, lastName);
}
}
public async Task<ErrorOr<User>> CreateUserAsync(string name)
{
if (await _userRepository.GetAsync(name) is User user)
{
return Error.Conflict("User already exists");
}
var errorOrUser = User.Create("Amichai");
if (errorOrUser.IsError)
{
return errorOrUser.Errors;
}
await _userRepository.AddAsync(errorOrUser.Value);
return errorOrUser.Value;
}
A more practical example
[HttpGet("{id:guid}")]
public async Task<IActionResult> GetUser(Guid Id)
{
var getUserQuery = new GetUserQuery(Id);
ErrorOr<User> getUserResponse = await _mediator.Send(getUserQuery);
return getUserResponse.Match(
user => Ok(_mapper.Map<UserResponse>(user)),
errors => ValidationProblem(errors.ToModelStateDictionary()));
}
A nice approach, is creating a static class with the expected errors. For example:
public static partial class Errors
{
public static class User
{
public static Error NotFound = Error.NotFound("User.NotFound", "User not found.");
public static Error DuplicateEmail = Error.Conflict("User.DuplicateEmail", "User with given email already exists.");
}
}
Which can later be used as following
User newUser = ..;
if (await _userRepository.GetByEmailAsync(newUser.email) is not null)
{
return Errors.User.DuplicateEmail;
}
await _userRepository.AddAsync(newUser);
return newUser;
Then, in an outer layer, you can use the Error.Match
method to return the appropriate HTTP status code.
return createUserResult.MatchFirst(
user => CreatedAtRoute("GetUser", new { id = user.Id }, user),
error => error is Errors.User.DuplicateEmail ? Conflict() : InternalServerError());
Dropping the exceptions throwing logic
You have validation logic such as MediatR
behaviors, you can drop the exceptions throwing logic and simply return a list of errors from the pipeline behavior
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
where TResponse : IErrorOr
{
private readonly IValidator<TRequest>? _validator;
public ValidationBehavior(IValidator<TRequest>? validator = null)
{
_validator = validator;
}
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
if (_validator == null)
{
return await next();
}
var validationResult = await _validator.ValidateAsync(request, cancellationToken);
if (validationResult.IsValid)
{
return await next();
}
return TryCreateResponseFromErrors(validationResult.Errors, out var response)
? response
: throw new ValidationException(validationResult.Errors);
}
private static bool TryCreateResponseFromErrors(List<ValidationFailure> validationFailures, out TResponse response)
{
List<Error> errors = validationFailures.ConvertAll(x => Error.Validation(
code: x.PropertyName,
description: x.ErrorMessage));
response = (TResponse?)typeof(TResponse)
.GetMethod(
name: nameof(ErrorOr<object>.From),
bindingAttr: BindingFlags.Static | BindingFlags.Public,
types: new[] { typeof(List<Error>) })?
.Invoke(null, new[] { errors })!;
return response is not null;
}
}
Usage
ErrorOr<result>
Creating an There are implicit converters from TResult
, Error
, List<Error>
to ErrorOr<TResult>
From Value, using implicit conversion
ErrorOr<int> result = 5;
public ErrorOr<int> GetValue()
{
return 5;
}
ErrorOrFactory.From
From Value, using ErrorOr<int> result = ErrorOrFactory.From(5);
public ErrorOr<int> GetValue()
{
return ErrorOrFactory.From(5);
}
From Single Error
ErrorOr<int> result = Error.Unexpected();
public ErrorOr<int> GetValue()
{
return Error.Unexpected();
}
From List of Errors, using implicit conversion
ErrorOr<int> result = new List<Error> { Error.Unexpected(), Error.Validation() };
public ErrorOr<int> GetValue()
{
return new List<Error>
{
Error.Unexpected(),
Error.Validation()
};
}
From
From List of Errors, using ErrorOr<int> result = ErrorOr<int>.From(new List<Error> { Error.Unexpected(), Error.Validation() });
public ErrorOr<int> GetValue()
{
return ErrorOr<int>.From(List<Error>
{
Error.Unexpected(),
Error.Validation()
};
}
ErrorOr<result>
is an error
Checking if the if (errorOrResult.IsError)
{
// errorOrResult is an error
}
ErrorOr<result>
result
Accessing the result.Value
)
Accessing the Value (ErrorOr<int> result = 5;
var value = result.Value;
result.Errors
)
Accessing the List of Errors (ErrorOr<int> result = new List<Error> { Error.Unexpected(), Error.Validation() };
List<Error> value = result.Errors; // List<Error> { Error.Unexpected(), Error.Validation() }
ErrorOr<int> result = Error.Unexpected();
List<Error> value = result.Errors; // List<Error> { Error.Unexpected() }
result.FirstError
)
Accessing the First Error (ErrorOr<int> result = new List<Error> { Error.Unexpected(), Error.Validation() };
Error value = result.FirstError; // Error.Unexpected()
ErrorOr<int> result = Error.Unexpected();
Error value = result.FirstError; // Error.Unexpected()
result.ErrorsOrEmptyList
)
Accessing the Errors or an empty list (ErrorOr<int> result = new List<Error> { Error.Unexpected(), Error.Validation() };
List<Error> errors = result.ErrorsOrEmptyList; // List<Error> { Error.Unexpected(), Error.Validation() }
ErrorOr<int> result = ErrorOrFactory.From(5);
List<Error> errors = result.ErrorsOrEmptyList; // List<Error> { }
ErrorOr<result>
result
Performing actions based on the Match
/ MatchAsync
Actions that return a value on the value or list of errors
string foo = errorOrString.Match(
value => value,
errors => $"{errors.Count} errors occurred.");
string foo = await errorOrString.MatchAsync(
value => Task.FromResult(value),
errors => Task.FromResult($"{errors.Count} errors occurred."));
MatchFirst
/ MatchFirstAsync
Actions that return a value on the value or first error
string foo = errorOrString.MatchFirst(
value => value,
firstError => firstError.Description);
string foo = await errorOrString.MatchFirstAsync(
value => Task.FromResult(value),
firstError => Task.FromResult(firstError.Description));
Switch
/ SwitchAsync
Actions that don't return a value on the value or list of errors
errorOrString.Switch(
value => Console.WriteLine(value),
errors => Console.WriteLine($"{errors.Count} errors occurred."));
await errorOrString.SwitchAsync(
value => { Console.WriteLine(value); return Task.CompletedTask; },
errors => { Console.WriteLine($"{errors.Count} errors occurred."); return Task.CompletedTask; });
SwitchFirst
/ SwitchFirstAsync
Actions that don't return a value on the value or first error
errorOrString.SwitchFirst(
value => Console.WriteLine(value),
firstError => Console.WriteLine(firstError.Description));
await errorOrString.SwitchFirstAsync(
value => { Console.WriteLine(value); return Task.CompletedTask; },
firstError => { Console.WriteLine(firstError.Description); return Task.CompletedTask; });
Error Types
Built-in Error Types
Each error has a type out of the following options:
public enum ErrorType
{
Failure,
Unexpected,
Validation,
Conflict,
NotFound,
}
Creating a new Error instance is done using one of the following static methods:
public static Error Error.Failure(string code, string description);
public static Error Error.Unexpected(string code, string description);
public static Error Error.Validation(string code, string description);
public static Error Error.Conflict(string code, string description);
public static Error Error.NotFound(string code, string description);
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.",
};
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
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(code: "User.NotFound", description: "User not found.");
}
await _userRepository.DeleteAsync(user);
return Result.Deleted;
}
OneOf<T0, T1>
or FluentResults
?
How Is This Different From It's similar to the others, just aims to be more intuitive and fluent.
If you find yourself typing OneOf<User, DomainError>
or Result.Fail<User>("failure")
again and again, you might enjoy the fluent API of ErrorOr<User>
(and it's also faster).
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.