Zentient.Results Overview - ulfbou/Zentient.Results GitHub Wiki
Zentient.Results
is a modern .NET library designed to bring clarity, consistency, and robustness to how your applications handle the outcomes of operations. Instead of relying solely on exceptions for every potential failure, Zentient.Results
provides explicit, immutable types that clearly communicate whether an operation succeeded or failed, and why.
Traditional error handling often leads to:
-
Ambiguous Outcomes: A
void
method doesn't tell you if it succeeded or failed, forcing implicit assumptions ortry-catch
blocks. - Unstructured Errors: Exceptions provide stack traces, but often lack business-specific context like error codes, categories, or associated data.
-
Complex Control Flow: Chaining operations where each might fail gracefully becomes cumbersome with nested
if
statements or scattered error checks.
Zentient.Results
addresses these by making success and failure first-class citizens in your method signatures and data structures.
The library revolves around a few key types:
-
IResult<T>
andResult<T>
: Used for operations that produce a value (T
) upon success. For example,IResult<User> GetUserById(Guid id)
. -
IResult
andResult
: Used for operations that don't produce a specific value (like avoid
method), but still need to indicate success or failure. For example,IResult DeleteProduct(string productId)
. -
ErrorInfo
: A structured, immutable object that provides detailed context for failures. It includes aCategory
(e.g.,Validation
,NotFound
), aCode
(e.g.,"INVALID_EMAIL"
), aMessage
(human-readable), and optionalData
for extra context. -
IResultStatus
andResultStatuses
: Provides a high-level status for the operation, often aligning with HTTP status codes (e.g.,200 OK
,404 Not Found
).ResultStatuses
offers a collection of predefined common statuses.
You create Result
objects using static factory methods:
-
Success with a Value (
Result<T>
):return Result<User>.Success(newUser); return Result<Order>.Created(newOrder); // For 201 Created
-
Success without a Value (
Result
):return Result.Success(); return Result.Success(ResultStatuses.NoContent); // For 204 No Content
-
Failure (for both
Result<T>
andResult
): You provide anErrorInfo
and anIResultStatus
. Convenience methods exist for common failures.// Generic failure return Result<Product>.Failure( default, new ErrorInfo(ErrorCategory.Validation, "InvalidSku", "SKU format is incorrect."), ResultStatuses.BadRequest ); // Convenience failure methods return Result<User>.NotFound(new ErrorInfo(ErrorCategory.NotFound, "UserNotFound", "User not found.")); return Result.Forbidden(new ErrorInfo(ErrorCategory.Authorization, "AccessDenied", "Permission denied."));
After receiving a Result
object, you check its state:
IResult<User> result = userService.GetUserById(userId);
if (result.IsSuccess)
{
User user = result.Value; // Access the successful value
Console.WriteLine($"User found: {user.Name}");
}
else
{
Console.WriteLine($"Operation failed: {result.Error}"); // Get the first error message
Console.WriteLine($"Status: {result.Status.Code} - {result.Status.Description}"); // Get high-level status
foreach (var error in result.Errors) // Iterate through all detailed errors
{
Console.WriteLine($"- Error ({error.Category}/{error.Code}): {error.Message}");
}
}
For exhaustive handling, the Match
method allows you to define distinct actions for success and failure:
service.GetProductById(productId).Match(
onSuccess: product => Console.WriteLine($"Product: {product.Name}"),
onFailure: errors => Console.Error.WriteLine($"Failed: {errors.First().Message}")
);
Zentient.Results
provides a fluent API to compose operations, significantly reducing boilerplate and improving readability:
-
Map
: Transforms the successful value of aResult<T>
into a new typeU
. If the result is a failure, it's simply propagated.IResult<int> userAge = GetRawData(userId).Map(jsonData => int.Parse(jsonData.Split(':')[1]));
-
Bind
: Chains an operation that also returns aResult
. If the preceding operation succeeded, theBind
function executes; otherwise, the failure is propagated. This is crucial for sequential, fallible steps.IResult<Discount> discount = GetCustomer(customerId).Bind(customer => CheckEligibility(customer));
-
OnSuccess
/OnFailure
: Allows you to perform side effects (like logging or dispatching events) only when the result is successful or failed, respectively, without altering the result itself.SaveData(data) .OnSuccess(savedData => Console.WriteLine($"Data saved: {savedData.Length} chars.")) .OnFailure(errors => Console.Error.WriteLine($"Failed to save: {errors.First().Message}"));
By adopting Zentient.Results
, you gain:
- Explicit Contracts: Methods clearly state their possible outcomes.
- Consistent Error Handling: A uniform way to communicate and consume errors across your application.
- Improved Readability: Fluent API makes complex workflows easier to understand.
- Enhanced Testability: Easier to write tests that assert specific success or failure states.
- Better Observability: Rich error details facilitate logging, monitoring, and debugging.
Zentient.Results
empowers you to build more robust, maintainable, and predictable .NET applications.