- 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));
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 ErrorOr<User> CreateUser(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));
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.