- Give it a star ⭐!
- Getting Started
- A more practical example
- 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()));
}
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, TResult> : IPipelineBehavior<TRequest, ErrorOr<TResult>>
where TRequest : IRequest<ErrorOr<TResult>>
{
private readonly IValidator<TRequest>? _validator;
public ValidationBehavior(IValidator<TRequest>? validator = null)
{
_validator = validator;
}
public async Task<ErrorOr<TResult>> Handle(
TRequest request,
CancellationToken cancellationToken,
RequestHandlerDelegate<ErrorOr<TResult>> next)
{
if (_validator == null)
{
return await next();
}
var validationResult = _validator.Validate(request);
if (validationResult.IsError)
{
return validationResult.Errors
.ConvertAll(validationFailure => Error.Validation(
code: validationFailure.PropertyName,
description: validationFailure.ErrorMessage));
}
return await next();
}
}
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.
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.
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());
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.