This package is a back ported version of ErrorOr to .NET Framework 4.8 to facilitate using it on legacy code projects. The goal is to be able to use it on legacy projects and enable upgrade to modern versions of .NET that can use the original library without needing code modifications.
dotnet add package ErrorOrDotNetFramework
- 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));
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,
}
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 ๐
This project is licensed under the terms of the MIT license.