I've got some news that might revolutionize how you handle errors and control your application flow in C#.
Have you ever felt that traditional error-handling methods and libraries don’t cut it anymore? Meet ErrorOr
, a C# library designed to simplify and streamline error handling with a unified approach.
ErrorOr
is a C# library designed to streamline error handling by offering a fluent and intuitive interface to represent a successful result or an error. Rather than dealing with exceptions or complex conditional structures, developers can use ErrorOr
to encapsulate both outcomes in a single, unified object.
Key Features
- Fluent API: Easily represent results or errors using intuitive methods and conversions.
- Built-in Error Types: Categorize errors using predefined types such as Failure, Unexpected, Validation, and more.
- Custom Error Types: Create error categories tailored to your application's needs.
- Seamless Integration: Works hand-in-hand with popular libraries like FluentValidation.
- Unified Error Handling: Simplify error-checking logic and enhance code readability.
Exception flow vs. ErrorOr flow
The side-by-side comparison below illustrates how ErrorOr
provides a more structured and intuitive approach to error handling, moving away from the pitfalls of exception-based control flow.
Exception flow
User GetUser(Guid id = default)
{
if (id == default)
{
throw new ValidationException("Id is required");
}
return new User(Name: "Mario Smolcic");
}
try
{
var user = GetUser();
Console.WriteLine(user.Name);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
ErrorOr flow
ErrorOr<User> GetUser(Guid id = default)
{
if (id == default)
{
return Error.Validation("Id is required");
}
return new User(Name: "Mario Smolcic");
}
var errorOrUser = GetUser();
errorOrUser.SwitchFirst(
user => Console.WriteLine(user.Name),
error => Console.WriteLine(error.Description));
Predefined and Custom Error Types
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);
public static class AuthorizationErrorTypes
{
public const int Unauthorized = 401;
}
public static class Errors
{
public static Error NotFound => Error.NotFound(
"Entity.NotFound",
"Entity not found.");
public static Error Unauthorized => Error.Custom(
AuthorizationErrorTypes.Unauthorized,
"Authorization.Unauthorized",
"Unauthorized.");
}
ErrorOr
not only comes with predefined error types for common scenarios but also empowers you to define custom error categories tailored to your application's specific needs.
Built-in Result Types
ErrorOr<Success> result = Result.Success;
ErrorOr<Created> result = Result.Created;
ErrorOr<Updated> result = Result.Updated;
ErrorOr<Deleted> result = Result.Deleted;
// Example
ErrorOr<Deleted> DeleteUser(Guid id)
{
var user = await _userRepository.GetByIdAsync(id);
if (user is null)
{
return Errors.NotFound;
}
await _userRepository.DeleteAsync(user);
return Result.Deleted;
}
The library also contains the usual CRUD operation result types. You can embrace its simplicity without compromising the granularity of the response.
Endpoint example
protected IActionResult HandleErrors(List<Error> errors)
{
if (errors.Any(e => e.NumericType == AuthorizationErrorTypes.Unauthorized))
{
return Unauthorized();
}
if (errors.Any(e => e.Type == ErrorType.Unexpected))
{
return Problem();
}
if (errors.All(e => e.Type == ErrorType.Validation))
{
var modelStateDictionary = new ModelStateDictionary();
foreach (var error in errors)
{
modelStateDictionary.AddModelError(error.Code, error.Description);
}
return ValidationProblem(modelStateDictionary);
}
var firstError = errors.First();
var statusCode = firstError.Type switch
{
ErrorType.NotFound => StatusCodes.Status404NotFound,
ErrorType.Validation => StatusCodes.Status400BadRequest,
ErrorType.Conflict => StatusCodes.Status409Conflict,
_ => StatusCodes.Status500InternalServerError
};
return Problem(statusCode: statusCode, title: firstError.Description);
}
[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 => HandleErrors);
}
- Error Representation: The
HandleErrors
method accepts a list of errors. An appropriate HTTP response is generated based on the nature of these errors (like authorization issues, unexpected errors, or validation problems). - Functional Handling: The
GetUser
method showcases a functional paradigm. By using theErrorOr<User>
type, which is reminiscent of theEither
monad in functional languages, the method can elegantly swap between the success and failure scenarios. - Expressiveness: Using methods like
Match
simplifies error handling, making the code more readable and maintainable.
Using FluentValidation and MediatR? No problem!
There's a smooth way to integrate ErrorOr
in the regular FluentValidation
flow with MediatR
and return the ErrorOr
result explicitly instead of throwing an error as usual. Let's take a look:
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;
}
}
- Avoid Exceptions for Control Flow: Instead of using exceptions to manage the control flow, return an error response. This aligns with the idea of treating exceptions as "exceptional" scenarios.
- Reflection for Dynamic Creation: The
TryCreateResponseFromErrors
method demonstrates a neat trick to create a response dynamically using reflection. This ensures ourValidationBehavior
remains generic and can work with any response type that conforms to ourIErrorOr
pattern. - Integration with FluentValidation: The behavior integrates smoothly with the
FluentValidation
library. If a request isn't valid, it attempts to generate an error response. If it can't, it defaults to throwing a validation exception.
In Conclusion
The world of error handling in C# has seen its fair share of practices. From the age-old try-catch blocks to throwing specific exceptions, we've always looked for more streamlined, efficient methods. The ErrorOr
library brings to the table an evolved, intuitive approach, bridging the gap between traditional error handling and the demands of modern application development. By embracing this library, you can ensure not just cleaner code but also a consistent error-handling strategy that integrates seamlessly with popular tools like FluentValidation and MediatR.
As technology evolves, so should our methodologies. Instead of relying on exceptions as a primary means of control flow, explore the capabilities of ErrorOr
. It could be a breath of fresh air for your codebase, aligning it more with functional programming paradigms and improving readability and maintainability.
The library source code can be found here.