Zentient.Results Overview - ulfbou/Zentient.Results GitHub Wiki

Zentient.Results: A Concise Overview

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.

The Problem It Solves

Traditional error handling often leads to:

  • Ambiguous Outcomes: A void method doesn't tell you if it succeeded or failed, forcing implicit assumptions or try-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.

Core Components

The library revolves around a few key types:

  • IResult<T> and Result<T>: Used for operations that produce a value (T) upon success. For example, IResult<User> GetUserById(Guid id).
  • IResult and Result: Used for operations that don't produce a specific value (like a void 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 a Category (e.g., Validation, NotFound), a Code (e.g., "INVALID_EMAIL"), a Message (human-readable), and optional Data for extra context.
  • IResultStatus and ResultStatuses: 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.

Basic Usage: Creating and Consuming Results

Creating Results

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> and Result): You provide an ErrorInfo and an IResultStatus. 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."));

Consuming Results

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}")
);

Chaining and Composing Operations

Zentient.Results provides a fluent API to compose operations, significantly reducing boilerplate and improving readability:

  • Map: Transforms the successful value of a Result<T> into a new type U. 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 a Result. If the preceding operation succeeded, the Bind 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}"));

Benefits

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.

⚠️ **GitHub.com Fallback** ⚠️