- 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
Loving it? Show your support by giving this project a star!
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);
}
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));
void AddUser(User user)
{
if (!_users.TryAdd(user))
{
throw new Exception("Failed to add user");
}
}
ErrorOr<Created> AddUser(User user)
{
if (!_users.TryAdd(user))
{
return Error.Failure(description: "Failed to add user");
}
return Results.Created;
}
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;
}
[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());
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,
CancellationToken cancellationToken,
RequestHandlerDelegate<TResponse> next)
{
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;
}
}
There are implicit converters from TResult
, Error
, List<Error>
to ErrorOr<TResult>
ErrorOr<int> result = 5;
public ErrorOr<int> GetValue()
{
return 5;
}
ErrorOr<int> result = Error.Unexpected();
public ErrorOr<int> GetValue()
{
return Error.Unexpected();
}
ErrorOr<int> result = new List<Error> { Error.Unexpected(), Error.Validation() };
public ErrorOr<int> GetValue()
{
return new List<Error>
{
Error.Unexpected(),
Error.Validation()
};
}
if (errorOrResult.IsError)
{
// errorOrResult is an error
}
ErrorOr<int> result = 5;
var value = result.Value;
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() }
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()
Actions that return a value on the value or list of errors
string foo = errorOrString.Match(
value => value,
errors => $"{errors.Count} errors occurred.");
Actions that return a value on the value or first error
string foo = errorOrString.MatchFirst(
value => value,
firstError => firstError.Description);
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."));
Actions that don't return a value on the value or first error
errorOrString.SwitchFirst(
value => Console.WriteLine(value),
firstError => Console.WriteLine(firstError.Description));
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.
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.",
};
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.
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;
}
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).
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.