Zentient Results Api Reference Result Types - ulfbou/Zentient.Results GitHub Wiki

🧱 Result Types


Zentient.Results provides core types to encapsulate the outcome of an operation, clearly distinguishing between success and various forms of failure. These types are designed to be immutable and to support functional composition patterns.

IResult Interface

The IResult interface is the non-generic base contract for all operation outcomes within Zentient.Results. It defines common properties to indicate success or failure and provides access to associated errors, messages, and status information. It is primarily used for operations that do not return a specific value upon success, but only convey the outcome of the action itself (e.g., a void method equivalent).

namespace Zentient.Results
{
    public interface IResult
    {
        /// <summary>
        /// Gets a value indicating whether the result represents a successful outcome.
        /// </summary>
        bool IsSuccess { get; }

        /// <summary>
        /// Gets a value indicating whether the result represents a failed outcome.
        /// This is the inverse of <see cref="IsSuccess"/>.
        /// </summary>
        bool IsFailure { get; }

        /// <summary>
        /// Gets a read-only list of structured error information.
        /// This list will be empty if <see cref="IsSuccess"/> is <c>true</c>.
        /// </summary>
        IReadOnlyList<ErrorInfo> Errors { get; }

        /// <summary>
        /// Gets a read-only list of general messages associated with the result,
        /// which can be present for both successful and failed outcomes.
        /// </summary>
        IReadOnlyList<string> Messages { get; }

        /// <summary>
        /// Gets the message of the first <see cref="ErrorInfo"/> in the <see cref="Errors"/> list, if any.
        /// Returns <c>null</c> if no errors are present or if the result is successful.
        /// </summary>
        string? Error { get; }

        /// <summary>
        /// Gets the overall status of the operation, providing a structured
        /// representation of the outcome (e.g., HTTP status code mapping).
        /// </summary>
        IResultStatus Status { get; }
    }
}

Properties

  • IsSuccess: bool

    • Description: Returns true if the operation completed successfully; otherwise, false. A result is considered successful if its Status indicates success (typically a 2xx status code) and its Errors collection is empty.
    • Usage: Useful for quick checks in conditional logic.
    • Example: if (result.IsSuccess) { /* handle success */ }
  • IsFailure: bool

    • Description: Returns true if the operation failed; otherwise, false. This is always the logical negation of IsSuccess.
    • Usage: Provides an alternative way to check for failure.
    • Example: if (result.IsFailure) { /* handle failure */ }
  • Errors: IReadOnlyList<ErrorInfo>

    • Description: A collection of ErrorInfo objects, each providing structured details about a specific error that occurred during the operation. This list will be empty for successful results.
    • Usage: Iterate through this collection to display or log detailed error information.
    • See Also: ErrorInfo Struct
    • Example: foreach (var error in result.Errors) { Console.WriteLine($"{error.Code}: {error.Message}"); }
  • Messages: IReadOnlyList<string>

    • Description: A collection of general informational or warning messages associated with the result. These messages can be present for both successful and failed outcomes and are typically less critical than Errors.
    • Usage: Display user-friendly messages regardless of success or failure.
    • Example: result.Messages.ForEach(m => Console.WriteLine(m));
  • Error: string?

    • Description: A convenience property that returns the Message of the first ErrorInfo in the Errors list. If the Errors list is empty (e.g., for a successful result), it returns null.
    • Usage: Quick access to a primary error message for display.
    • Example: Console.WriteLine($"First error: {result.Error}");
  • Status: IResultStatus

    • Description: Represents the overall status of the operation. This can include a numeric code (like an HTTP status code) and a descriptive message. The status is crucial for categorizing the outcome (e.g., Ok, NotFound, ValidationFailure).
    • Usage: Determine the semantic outcome of the operation beyond just success/failure.
    • See Also: IResultStatus Interface, ResultStatuses Static Class
    • Example: if (result.Status == ResultStatuses.NotFound) { /* specific handling */ }

IResult<T> Interface

The IResult<T> interface extends IResult by adding the capability to encapsulate a successful value of type T. It is the primary interface for operations that are expected to return data upon success, while still providing robust error handling for failure scenarios.

namespace Zentient.Results
{
    public interface IResult<T> : IResult
    {
        /// <summary>
        /// Gets the successful value encapsulated by the result.
        /// This property will be <c>null</c> or <c>default(T)</c> if <see cref="IsFailure"/> is <c>true</c>.
        /// </summary>
        T? Value { get; }

        /// <summary>
        /// Retrieves the value if the result is successful, otherwise throws an <see cref="InvalidOperationException"/>.
        /// </summary>
        /// <exception cref="InvalidOperationException">Thrown if the result is a failure.</exception>
        T GetValueOrThrow();

        /// <summary>
        /// Retrieves the value if the result is successful, otherwise throws an <see cref="InvalidOperationException"/>
        /// with a custom message.
        /// </summary>
        /// <param name="message">The custom message for the exception.</param>
        /// <exception cref="InvalidOperationException">Thrown if the result is a failure.</exception>
        T GetValueOrThrow(string message);

        /// <summary>
        /// Retrieves the value if the result is successful, otherwise throws an exception created by the provided factory.
        /// </summary>
        /// <param name="exceptionFactory">A function that returns an exception to be thrown on failure.</param>
        /// <exception cref="Exception">The exception created by the provided factory.</exception>
        T GetValueOrThrow(Func<Exception> exceptionFactory);

        /// <summary>
        /// Retrieves the value if the result is successful, otherwise returns a specified fallback value.
        /// </summary>
        /// <param name="fallback">The value to return if the result is a failure.</param>
        /// <returns>The successful value or the fallback value.</returns>
        T GetValueOrDefault(T fallback);

        /// <summary>
        /// Transforms the successful value of the result to a new result type <typeparamref name="U"/>
        /// using the provided selector function. If the current result is a failure, it propagates the failure.
        /// This is useful for chaining operations that return <see cref="IResult{U}"/>.
        /// </summary>
        /// <typeparam name="U">The type of the new result's value.</typeparam>
        /// <param name="selector">A function to transform the successful value.</param>
        /// <returns>A new <see cref="IResult{U}"/> instance.</returns>
        IResult<U> Map<U>(Func<T, U> selector);

        /// <summary>
        /// Flat-maps the successful value of the result to a new <see cref="IResult{U}"/>
        /// using the provided binder function. If the current result is a failure, it propagates the failure.
        /// This is useful for chaining operations that themselves return <see cref="IResult{U}"/>.
        /// </summary>
        /// <typeparam name="U">The type of the new result's value.</typeparam>
        /// <param name="binder">A function to transform the successful value into a new result.</param>
        /// <returns>A new <see cref="IResult{U}"/> instance.</returns>
        IResult<U> Bind<U>(Func<T, IResult<U>> binder);

        /// <summary>
        /// Executes a side-effect action if the result is successful, then returns the original result.
        /// </summary>
        /// <param name="onSuccess">The action to execute if the result is successful.</param>
        /// <returns>The original <see cref="IResult{T}"/> instance.</returns>
        IResult<T> Tap(Action<T> onSuccess);

        /// <summary>
        /// Executes an action if the result is successful, then returns the original result.
        /// This is an alias for <see cref="Tap(Action{T})"/>.
        /// </summary>
        /// <param name="action">The action to execute on success.</param>
        /// <returns>The original <see cref="IResult{T}"/> instance.</returns>
        IResult<T> OnSuccess(Action<T> action);

        /// <summary>
        /// Executes an action if the result is a failure, then returns the original result.
        /// </summary>
        /// <param name="action">The action to execute on failure, receiving the list of errors.</param>
        /// <returns>The original <see cref="IResult{T}"/> instance.</returns>
        IResult<T> OnFailure(Action<IReadOnlyList<ErrorInfo>> action);

        /// <summary>
        /// Transforms the result into a new type <typeparamref name="U"/> by applying one of two functions,
        /// depending on whether the result is successful or a failure.
        /// </summary>
        /// <typeparam name="U">The type to which the result will be transformed.</typeparam>
        /// <param name="onSuccess">The function to apply if the result is successful, receiving the value.</param>
        /// <param name="onFailure">The function to apply if the result is a failure, receiving the list of errors.</param>
        /// <returns>The transformed value of type <typeparamref name="U"/>.</returns>
        U Match<U>(Func<T, U> onSuccess, Func<IReadOnlyList<ErrorInfo>, U> onFailure);
    }
}

Properties

  • Value: T?
    • Description: The successful value encapsulated by the result. This property will be default(T) (e.g., null for reference types, 0 for int, etc.) if IsFailure is true. Accessing Value when IsFailure is true is a common source of bugs if not handled carefully. Always check IsSuccess or use helper methods like GetValueOrThrow() or GetValueOrDefault().
    • Usage: Retrieve the actual data from a successful result.
    • Example: if (result.IsSuccess) { var data = result.Value; }

Methods

(Detailed explanations and practical examples for Map, Bind, Then, Tap, OnSuccess, OnFailure, and Match are provided in the Functional Utilities page. Below are brief descriptions focusing on their core purpose.)

  • GetValueOrThrow() (and overloads)

    • Description: Retrieves the Value if IsSuccess is true. If IsFailure is true, it throws an InvalidOperationException (or a custom exception with overloads).
    • Usage: Use when you are certain the result will be successful, or when a failure should explicitly halt execution (e.g., during startup configuration).
    • See Also: Inspection & Querying
  • GetValueOrDefault(T fallback)

    • Description: Retrieves the Value if IsSuccess is true. If IsFailure is true, it returns the provided fallback value instead.
    • Usage: Provides a safe default value for failure scenarios, preventing exceptions.
    • See Also: Inspection & Querying
  • Map<U>(Func<T, U> selector)

    • Description: Transforms a successful IResult<T> into an IResult<U> by applying a selector function to the Value. If the original result was a failure, the failure is propagated without executing the selector.
    • Usage: Chain operations where each step produces a different successful value, but the overall result can still fail.
    • See Also: Map Extension Methods
  • Bind<U>(Func<T, IResult<U>> binder)

    • Description: Flat-maps a successful IResult<T> into an IResult<U> by applying a binder function that itself returns an IResult<U>. If the original result was a failure, the failure is propagated.
    • Usage: Chain operations where each step also produces a Result type, allowing for sequential validation or processing.
    • See Also: Bind Extension Methods
  • Tap(Action<T> onSuccess) / OnSuccess(Action<T> action)

    • Description: Executes a side-effect Action if the result is successful, then returns the original result instance. OnSuccess is an alias for Tap.
    • Usage: For logging, auditing, or other side-effects that don't change the result's value or success/failure state.
    • See Also: Tap Extension Method, OnSuccess Extension Methods
  • OnFailure(Action<IReadOnlyList<ErrorInfo>> action)

    • Description: Executes an Action if the result is a failure, receiving the list of errors, then returns the original result instance.
    • Usage: For logging errors, displaying error messages to the user, or other side-effects specific to failure scenarios.
    • See Also: OnFailure Extension Methods
  • Match<U>(Func<T, U> onSuccess, Func<IReadOnlyList<ErrorInfo>, U> onFailure)

    • Description: Transforms the result into a new type U by applying one of two functions: onSuccess if IsSuccess is true, or onFailure if IsFailure is true. This forces exhaustive handling of both success and failure paths.
    • Usage: Convert a Result into a UI model, a different data structure, or handle side-effects in a single expression.
    • See Also: Match Extension Method

Result Struct

The Result struct is the immutable, non-generic concrete implementation of the IResult interface. It's used for operations that only need to convey success or failure, without an associated return value. It provides a rich set of static factory methods for creating results.

namespace Zentient.Results
{
    public readonly struct Result : IResult, IEquatable<Result>
    {
        // Properties inherited from IResult (IsSuccess, IsFailure, Errors, Messages, Error, Status)
        // Internal constructor, not for public direct use.

        // Static factory methods for creation (e.g., Result.Success(), Result.Failure())
        // Implicit operator for ErrorInfo conversion
        // Overrides for Equals, GetHashCode
        // ...
    }
}

Key Characteristics

  • Immutable: Once a Result instance is created, its internal state (success/failure, errors, messages, status) cannot be changed. This promotes predictability and thread-safety.
  • Value Type: Being a readonly struct, Result instances are allocated on the stack (or inline in objects), reducing garbage collection pressure compared to reference types.
  • Factory-driven Creation: Result instances are almost exclusively created using the static factory methods provided on the Result class (e.g., Result.Success(), Result.Failure()). Direct instantiation via new Result(...) is discouraged and often not possible due to internal constructors.
  • Equality: Result implements IEquatable<Result>, allowing for value-based equality comparisons.

Static Factory Methods

The Result struct provides a comprehensive set of static factory methods to create various types of successful or failed results. These are detailed in the Result Builders & Factories page. Examples include:

  • Result.Success(): Creates a successful result.
  • Result.Success(string message): Creates a successful result with an associated message.
  • Result.Failure(ErrorInfo error): Creates a failed result with a single ErrorInfo.
  • Result.Validation(IEnumerable<ErrorInfo> errors): Creates a failed result specifically for validation errors.
  • Result.NotFound(string resourceName): Creates a failed result indicating a resource was not found.

Implicit Conversions

For convenience, Result supports implicit conversions:

  • ErrorInfo to Result: Allows you to directly return an ErrorInfo object where a Result is expected, which will implicitly convert to a failed Result.
    public Result DoSomething()
    {
        return new ErrorInfo(ErrorCategory.BusinessLogic, "InvalidState", "System is in invalid state.");
    }

Result<T> Struct

The Result<T> struct is the immutable, generic concrete implementation of the IResult<T> interface. It is designed for operations that return a specific value of type T upon success, alongside comprehensive error handling for failure scenarios. It combines the benefits of the Result struct with the ability to carry a payload.

namespace Zentient.Results
{
    public readonly struct Result<T> : IResult<T>, IEquatable<Result<T>>
    {
        // Properties inherited from IResult and IResult<T> (Value, IsSuccess, IsFailure, Errors, Messages, Error, Status)
        // Internal constructor, not for public direct use.

        // Static factory methods for creation (e.g., Result.Success<T>(value), Result.Failure<T>(errors))
        // Implicit operator for T value conversion
        // Overrides for Equals, GetHashCode
        // Implementations of IResult<T> methods (Map, Bind, GetValueOrThrow, etc.)
        // ...
    }
}

Key Characteristics

  • Immutable: Like the non-generic Result, Result<T> instances are immutable.
  • Value Type: As a readonly struct, it offers performance benefits by reducing heap allocations and GC overhead.
  • Factory-driven Creation: Created via static factory methods, often on the non-generic Result class (e.g., Result.Success<T>(value)) or implicit conversions.
  • Payload Carrying: Safely encapsulates a value of type T when the operation is successful.
  • Equality: Result<T> implements IEquatable<Result<T>>, enabling value-based equality comparisons, considering both the success/failure state, errors, and the Value (if successful).

Static Factory Methods

The Result<T> type also leverages the static factory methods from the Result class (or its own implicit conversions) to create instances. These are also covered in detail on the Result Builders & Factories page. Examples include:

  • Result.Success<T>(T value): Creates a successful result with a value.
  • Result.Failure<T>(ErrorInfo error): Creates a failed result for Result<T>.
  • Result.NotFound<T>(string resourceName): Creates a typed failure.

Implicit Conversions

For highly fluent syntax, Result<T> supports implicit conversions:

  • T to Result<T>: Allows you to directly return a value of type T where a Result<T> is expected, which will implicitly convert to a successful Result<T>.
    public IResult<int> ParseNumber(string text)
    {
        if (int.TryParse(text, out int number))
        {
            return number; // Implicitly converts to Result<int>.Success(number)
        }
        return Result.Validation<int>(new ErrorInfo[] {
            new ErrorInfo(ErrorCategory.Validation, "InvalidNumber", "Input is not a valid number.")
        });
    }
  • ErrorInfo to Result<T>: Similar to the non-generic Result, an ErrorInfo can implicitly convert to a failed Result<T>.
    public IResult<User> GetUserById(int id)
    {
        if (id <= 0)
        {
            return new ErrorInfo(ErrorCategory.Validation, "InvalidId", "User ID must be positive."); // Implicitly converts to Result<User>.Failure
        }
        // ... logic
        return Result.NotFound<User>("User", id.ToString()); // Explicit factory method example
    }

Functional Methods

Result<T> implements the core functional methods defined in IResult<T>, such as Map, Bind, Match, Tap, OnSuccess, and OnFailure. These methods are critical for chaining operations and handling outcomes in a functional style.

  • See Also: For detailed explanations and examples of how to use these powerful methods, refer to the Functional Utilities page.
⚠️ **GitHub.com Fallback** ⚠️