Zentient Results Api Reference Result Types - ulfbou/Zentient.Results GitHub Wiki
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.
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; }
}
}
-
IsSuccess
:bool
-
Description: Returns
true
if the operation completed successfully; otherwise,false
. A result is considered successful if itsStatus
indicates success (typically a 2xx status code) and itsErrors
collection is empty. - Usage: Useful for quick checks in conditional logic.
-
Example:
if (result.IsSuccess) { /* handle success */ }
-
Description: Returns
-
IsFailure
:bool
-
Description: Returns
true
if the operation failed; otherwise,false
. This is always the logical negation ofIsSuccess
. - Usage: Provides an alternative way to check for failure.
-
Example:
if (result.IsFailure) { /* handle failure */ }
-
Description: Returns
-
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}"); }
-
Description: A collection of
-
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));
-
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
-
Error
:string?
-
Description: A convenience property that returns the
Message
of the firstErrorInfo
in theErrors
list. If theErrors
list is empty (e.g., for a successful result), it returnsnull
. - Usage: Quick access to a primary error message for display.
-
Example:
Console.WriteLine($"First error: {result.Error}");
-
Description: A convenience property that returns the
-
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 */ }
-
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.,
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);
}
}
-
Value
:T?
-
Description: The successful value encapsulated by the result. This property will be
default(T)
(e.g.,null
for reference types,0
forint
, etc.) ifIsFailure
istrue
. AccessingValue
whenIsFailure
istrue
is a common source of bugs if not handled carefully. Always checkIsSuccess
or use helper methods likeGetValueOrThrow()
orGetValueOrDefault()
. - Usage: Retrieve the actual data from a successful result.
-
Example:
if (result.IsSuccess) { var data = result.Value; }
-
Description: The successful value encapsulated by the result. This property will be
(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
ifIsSuccess
istrue
. IfIsFailure
istrue
, it throws anInvalidOperationException
(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
-
Description: Retrieves the
-
GetValueOrDefault(T fallback)
-
Description: Retrieves the
Value
ifIsSuccess
istrue
. IfIsFailure
istrue
, it returns the providedfallback
value instead. - Usage: Provides a safe default value for failure scenarios, preventing exceptions.
- See Also: Inspection & Querying
-
Description: Retrieves the
-
Map<U>(Func<T, U> selector)
-
Description: Transforms a successful
IResult<T>
into anIResult<U>
by applying aselector
function to theValue
. If the original result was a failure, the failure is propagated without executing theselector
. - Usage: Chain operations where each step produces a different successful value, but the overall result can still fail.
- See Also: Map Extension Methods
-
Description: Transforms a successful
-
Bind<U>(Func<T, IResult<U>> binder)
-
Description: Flat-maps a successful
IResult<T>
into anIResult<U>
by applying abinder
function that itself returns anIResult<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
-
Description: Flat-maps a successful
-
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 forTap
. - 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
-
Description: Executes a side-effect
-
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
-
Description: Executes an
-
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
ifIsSuccess
istrue
, oronFailure
ifIsFailure
istrue
. 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
-
Description: Transforms the result into a new type
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
// ...
}
}
-
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 theResult
class (e.g.,Result.Success()
,Result.Failure()
). Direct instantiation vianew Result(...)
is discouraged and often not possible due to internal constructors. -
Equality:
Result
implementsIEquatable<Result>
, allowing for value-based equality comparisons.
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 singleErrorInfo
. -
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.
For convenience, Result
supports implicit conversions:
-
ErrorInfo
toResult
: Allows you to directly return anErrorInfo
object where aResult
is expected, which will implicitly convert to a failedResult
.public Result DoSomething() { return new ErrorInfo(ErrorCategory.BusinessLogic, "InvalidState", "System is in invalid state."); }
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.)
// ...
}
}
-
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>
implementsIEquatable<Result<T>>
, enabling value-based equality comparisons, considering both the success/failure state, errors, and theValue
(if successful).
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 forResult<T>
. -
Result.NotFound<T>(string resourceName)
: Creates a typed failure.
For highly fluent syntax, Result<T>
supports implicit conversions:
-
T
toResult<T>
: Allows you to directly return a value of typeT
where aResult<T>
is expected, which will implicitly convert to a successfulResult<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
toResult<T>
: Similar to the non-genericResult
, anErrorInfo
can implicitly convert to a failedResult<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 }
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.