Understanding IResult and IResult T - ulfbou/Zentient.Results GitHub Wiki

Understanding IResult and IResult<T>

At the heart of Zentient.Results are the IResult and IResult<T> interfaces, along with their concrete Result and Result<T> struct implementations. These are the fundamental building blocks that enable explicit, predictable, and robust outcome handling in your .NET applications.

This page will help you grasp what these types are, why they exist, and how they function.


1. The Core Idea: Explicit Outcomes

Traditionally, C# methods that perform an action might return void (implying success) or a direct value T (implying success), and communicate failures by throwing exceptions. While exceptions are vital for truly exceptional and unrecoverable errors, they can lead to tangled control flow when used for expected business outcomes (e.g., "user not found," "invalid input").

Zentient.Results introduces IResult and IResult<T> as explicit return types that declare: "This operation might succeed, or it might fail, and here's all the context if it does."

This approach provides:

  • Predictability: The method signature immediately tells you that you need to handle both success and failure.
  • Clarity: It's always clear whether an operation completed successfully or with an issue.
  • Structured Information: Failures carry rich, categorized error details, not just a stack trace.

2. IResult<T>: Operations with a Return Value

The generic IResult<T> interface (and its concrete implementation, Result<T>) is used for operations that, upon success, produce a specific value of type T.

Purpose: Use IResult<T> when your method computes or retrieves something concrete.

Example Use Cases:

  • IResult<User> GetUserById(Guid id)
  • IResult<Order> CreateOrder(OrderRequest request)
  • IResult<decimal> CalculateDiscount(decimal amount)

Key Properties of IResult<T>:

Property Type Description
IsSuccess bool true if the operation succeeded; false otherwise.
IsFailure bool true if the operation failed; false otherwise. (Convenient inverse of IsSuccess)
Value T? The successful result value. Only access if IsSuccess is true. Will be default(T) if IsFailure is true (or if explicitly passed to Failure methods).
Errors IReadOnlyList<ErrorInfo> A collection of detailed ErrorInfo objects, present only if IsFailure is true. Can contain multiple errors (e.g., from validation).
Error string? A convenience property that returns the Message of the first ErrorInfo in the Errors collection if available; otherwise null.
Messages IReadOnlyList<string> A collection of non-error informational messages associated with the result. Useful for warnings or additional context.
Status IResultStatus A high-level status object (e.g., ResultStatuses.Success, ResultStatuses.NotFound), often mapping to HTTP status codes. Provides an int Code and string Description.

Creating Result<T> Instances:

Result<T> provides numerous static factory methods to create instances, ensuring correct state initialization:

  • Success:
    return Result<User>.Success(newUser); // Common success with value
    return Result<Order>.Created(newOrder); // Success indicating resource creation (201)
  • Failure:
    // Generic failure:
    return Result<Product>.Failure(
        default, // Often default(T) for failure, but can pass a partial value if sensible
        new ErrorInfo(ErrorCategory.Validation, "InvalidSku", "SKU format is incorrect."),
        ResultStatuses.BadRequest
    );
    
    // Convenience methods for common failures:
    return Result<User>.NotFound(
        new ErrorInfo(ErrorCategory.NotFound, "UserNotFound", $"User with ID {userId} was not found.")
    );
    return Result<string>.Unauthorized(
        new ErrorInfo(ErrorCategory.Authentication, "TokenExpired", "Authentication token is expired.")
    );
    return Result<Customer>.Conflict(
        new ErrorInfo(ErrorCategory.Conflict, "EmailTaken", "Email already registered.", new { emailAddress })
    );

3. IResult: Operations Without a Specific Return Value

The non-generic IResult interface (and its concrete implementation, Result) is used for operations whose primary outcome is a state change or an action, rather than producing a specific return value. Think of void methods in traditional C#.

Purpose: Use IResult for "command" methods that might succeed or fail.

Example Use Cases:

  • IResult DeleteProduct(string productId)
  • IResult SendEmail(EmailMessage message)
  • IResult UpdateOrderStatus(Guid orderId, OrderStatus newStatus)

Key Properties of IResult:

IResult shares most properties with IResult<T>, excluding the Value property.

Property Type Description
IsSuccess bool true if the operation succeeded; false otherwise.
IsFailure bool true if the operation failed; false otherwise.
Errors IReadOnlyList<ErrorInfo> A collection of detailed ErrorInfo objects.
Error string? The Message of the first ErrorInfo if available.
Messages IReadOnlyList<string> A collection of non-error informational messages.
Status IResultStatus A high-level status object (e.g., ResultStatuses.Success, ResultStatuses.Forbidden).

Creating Result Instances:

Result also provides static factory methods:

  • Success:
    return Result.Success(); // Simple success (often maps to HTTP 200 OK)
    return Result.Success(ResultStatuses.NoContent); // Success indicating no content (HTTP 204)
  • Failure:
    // Generic failure:
    return Result.Failure(
        new ErrorInfo(ErrorCategory.Network, "ConnectionLost", "Failed to connect to external service."),
        ResultStatuses.InternalServerError
    );
    
    // Convenience methods for common failures:
    return Result.Validation( // Specifically for validation errors (often maps to HTTP 400)
        new ErrorInfo(ErrorCategory.Validation, "InvalidInput", "Input field 'x' is required.")
    );
    return Result.Forbidden(
        new ErrorInfo(ErrorCategory.Authorization, "AccessDenied", "User does not have permission to delete this resource.")
    );

4. Result and Result<T> as readonly struct

Both Result and Result<T> are implemented as readonly struct in C#. This is a deliberate design choice with several benefits:

  • Immutability: Once a Result or Result<T> instance is created, its internal state (whether it's a success or failure, its value, errors, and status) cannot be changed. This makes reasoning about your code easier, reduces potential bugs from unexpected side effects, and is crucial for thread-safe operations.
  • Performance: struct types are typically allocated on the stack (or inline within other objects) rather than the heap. This reduces pressure on the garbage collector, leading to better performance, especially in high-throughput applications where many result objects might be created and discarded.
  • Value Semantics: They behave like simple values (e.g., an int or a bool). When you pass a Result around, you are passing a copy of its immutable state.

5. The Status Property (IResultStatus)

The Status property (of type IResultStatus) provides a high-level classification of the outcome. It's primarily intended to align with well-understood codes, such as HTTP status codes, but can also be customized for domain-specific contexts.

Key Distinction: Status vs. Errors

  • Status (What happened?): A general categorization (e.g., 404 Not Found, 200 OK, 400 Bad Request). It tells you the type of overall outcome.
  • Errors (Why it happened?): Provides granular, detailed ErrorInfo objects that explain the specific reasons for a failure. An IResultStatus of BadRequest might correspond to multiple ErrorInfo instances detailing which input fields were invalid.

6. The Errors Property (IReadOnlyList<ErrorInfo>)

The Errors property holds a collection of ErrorInfo objects.

  • Multiple Errors: It's a list (IReadOnlyList<ErrorInfo>) because an operation can fail for multiple reasons simultaneously. For example, a validation process might identify several invalid input fields.
  • Structured Details: Each ErrorInfo provides a Category, Code, Message, and optional Data, offering rich context for diagnostics, logging, and user feedback.

7. Comparison: IResult<T> vs. IResult

Feature IResult<T> IResult
Primary Use Operations that produce a specific output value. Operations that primarily cause a side effect/state change.
Success Value Contains a Value of type T. Does not contain a value.
Example GetUserById(), CalculateTax() DeleteUser(), SendEmail()
Factory Methods Success(T value), Failure(T? value, ErrorInfo...) Success(), Failure(ErrorInfo...)
Functional Ops Supports Map, Bind (to transform T). Supports OnSuccess, OnFailure (for side effects).

When to Choose Which:

  • Choose IResult<T> when your method's main purpose is to return data that the caller needs to proceed.
  • Choose IResult when your method's main purpose is to perform an action, and the caller only needs to know if the action succeeded or failed.

Example Scenario: User Profile Management

using Zentient.Results;
using System;
using System.Collections.Generic;

public class UserProfileService
{
    private static Dictionary<Guid, UserProfile> _userProfiles = new Dictionary<Guid, UserProfile>();

    // IResult<T>: Get a user profile (produces a value)
    public IResult<UserProfile> GetProfile(Guid userId)
    {
        if (!_userProfiles.TryGetValue(userId, out var profile))
        {
            return Result<UserProfile>.NotFound(
                new ErrorInfo(ErrorCategory.NotFound, "UserProfileNotFound", $"Profile for user {userId} not found.")
            );
        }
        return Result<UserProfile>.Success(profile);
    }

    // IResult: Update a user profile (performs an action, no value returned)
    public IResult UpdateProfile(Guid userId, string newEmail)
    {
        if (string.IsNullOrWhiteSpace(newEmail) || !newEmail.Contains("@"))
        {
            return Result.BadRequest(
                new ErrorInfo(ErrorCategory.Validation, "InvalidEmail", "Invalid email format.")
            );
        }

        if (!_userProfiles.ContainsKey(userId))
        {
            return Result.NotFound(
                new ErrorInfo(ErrorCategory.NotFound, "UserProfileNotFound", $"Profile for user {userId} not found.")
            );
        }

        // Simulate update
        _userProfiles[userId].Email = newEmail;
        Console.WriteLine($"Profile for {userId} updated with new email: {newEmail}");
        return Result.Success();
    }

    // Helper for initial data
    public void AddProfile(UserProfile profile)
    {
        _userProfiles[profile.UserId] = profile;
    }
}

public class UserProfile
{
    public Guid UserId { get; set; }
    public string Username { get; set; }
    public string Email { get; set; }
}

public class Application
{
    public static void Main(string[] args)
    {
        var service = new UserProfileService();
        var userAId = Guid.NewGuid();
        var userBId = Guid.NewGuid();
        service.AddProfile(new UserProfile { UserId = userAId, Username = "Alice", Email = "[email protected]" });

        // GetProfile - Success
        var profileResult = service.GetProfile(userAId);
        if (profileResult.IsSuccess)
        {
            Console.WriteLine($"Found profile for: {profileResult.Value.Username} ({profileResult.Value.Email})");
        }

        // GetProfile - Failure (Not Found)
        var notFoundResult = service.GetProfile(userBId);
        if (notFoundResult.IsFailure)
        {
            Console.WriteLine($"Error getting profile ({notFoundResult.Status.Code}): {notFoundResult.Error}");
        }

        Console.WriteLine("\n--- Updating Profiles ---");

        // UpdateProfile - Success
        var updateSuccessResult = service.UpdateProfile(userAId, "[email protected]");
        if (updateSuccessResult.IsSuccess)
        {
            Console.WriteLine("Profile updated successfully!");
        }

        // UpdateProfile - Failure (Invalid Email)
        var updateInvalidEmailResult = service.UpdateProfile(userAId, "invalid-email");
        if (updateInvalidEmailResult.IsFailure)
        {
            Console.WriteLine($"Error updating profile ({updateInvalidEmailResult.Status.Code}): {updateInvalidEmailResult.Error}");
        }

        // UpdateProfile - Failure (Profile Not Found)
        var updateNotFoundResult = service.UpdateProfile(userBId, "[email protected]");
        if (updateNotFoundResult.IsFailure)
        {
            Console.WriteLine($"Error updating profile ({updateNotFoundResult.Status.Code}): {updateNotFoundResult.Error}");
        }
    }
}

Next Steps

Now that you understand the core IResult and IResult<T> types, explore these related topics:

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