Basic Usage Patterns - ulfbou/Zentient.Results GitHub Wiki

Basic Usage Patterns

This guide expands on the "Quick Start Guide" by diving deeper into common usage patterns for Zentient.Results. You'll find more detailed examples of how to create, consume, and manage result objects in various scenarios, making your code robust and expressive.


1. Returning Results from Methods

The fundamental pattern is to have your methods return an IResult<T> (or IResult for void operations) instead of T or throwing exceptions.

Example: Method with Conditional Success/Failure

This is the most common scenario: an operation that might succeed or fail based on business rules or external factors.

using Zentient.Results;
using System;
using System.Text.RegularExpressions;

public class UserRegistrationService
{
    // Simulates a user data store
    private static List<string> _registeredUsernames = new List<string> { "alice", "bob" };

    public IResult<User> RegisterUser(string username, string password)
    {
        // 1. Input Validation
        if (string.IsNullOrWhiteSpace(username) || username.Length < 3)
        {
            return Result<User>.BadRequest( // Convenient static method for 400
                new ErrorInfo(ErrorCategory.Validation, "InvalidUsername", "Username must be at least 3 characters and not empty.")
            );
        }
        if (!Regex.IsMatch(password, @"^(?=.*[A-Za-z])(?=.*d)[A-Za-zd]{8,}$"))
        {
            return Result<User>.BadRequest(
                new ErrorInfo(ErrorCategory.Validation, "WeakPassword", "Password must be at least 8 characters, contain letters and numbers.")
            );
        }

        // 2. Business Rule Validation (e.g., uniqueness)
        if (_registeredUsernames.Contains(username.ToLower()))
        {
            return Result<User>.Conflict( // Convenient static method for 409
                new ErrorInfo(ErrorCategory.Conflict, "UsernameTaken", $"Username '{username}' is already taken.")
            );
        }

        // 3. Simulate Successful Registration
        var newUser = new User { Id = Guid.NewGuid(), Username = username };
        _registeredUsernames.Add(username.ToLower());
        Console.WriteLine($"User '{username}' registered successfully.");
        return Result<User>.Created(newUser); // Convenient static method for 201
    }
}

public class User { public Guid Id { get; set; } public string Username { get; set; } }

2. Consuming Results (Handling Outcomes)

After a method returns a Result, you need to consume it to determine the next steps.

Basic if/else Checks

The most straightforward way to handle success or failure.

using Zentient.Results;

public class ClientApplication
{
    public void RunRegistrationFlow()
    {
        var service = new UserRegistrationService();

        // Scenario 1: Successful Registration
        IResult<User> successResult = service.RegisterUser("charlie", "SecureP@ss123");
        if (successResult.IsSuccess)
        {
            User registeredUser = successResult.Value; // Access the value
            Console.WriteLine($"Registration successful for user: {registeredUser.Username} (ID: {registeredUser.Id})");
        }
        else
        {
            Console.WriteLine($"Registration failed (Status: {successResult.Status.Code}): {successResult.Error}");
        }

        // Scenario 2: Validation Failure
        IResult<User> validationFailureResult = service.RegisterUser("jo", "pass"); // Too short username, weak password
        if (validationFailureResult.IsFailure)
        {
            Console.WriteLine($"Validation failed (Status: {validationFailureResult.Status.Code}): {validationFailureResult.Error}");
            foreach (var error in validationFailureResult.Errors)
            {
                Console.WriteLine($"- Error ({error.Code}): {error.Message}");
            }
        }

        // Scenario 3: Conflict Failure
        IResult<User> conflictFailureResult = service.RegisterUser("charlie", "AnotherP@ss123"); // Charlie already registered
        if (conflictFailureResult.IsFailure)
        {
            Console.WriteLine($"Conflict failed (Status: {conflictFailureResult.Status.Code}): {conflictFailureResult.Error}");
        }
    }
}

Accessing Value and Errors Safely

  • result.Value: Only access if result.IsSuccess is true.
  • result.Error: Provides the first error message if IsFailure is true.
  • result.Errors: Provides an IReadOnlyList<ErrorInfo> of all errors.

Using GetValueOrDefault

Provides a default value if the result is a failure, preventing null reference exceptions.

public class DataConsumer
{
    public string GetUserNameOrDefault(int userId)
    {
        var userService = new UserService(); // Assume GetUserName from Quick Start Guide

        IResult<string> userNameResult = userService.GetUserName(userId);

        // If success, returns the user name. If failure, returns "Guest".
        return userNameResult.GetValueOrDefault("Guest"); 
    }
}

Using ThrowIfFailure() (with Caution)

ThrowIfFailure() is an extension method that will throw a ResultException if the result is a failure. Use this when you genuinely want to stop execution if an operation fails, often at the boundaries of your application (e.g., API controllers after all business logic has run) or in CLI tools where unhandled exceptions are acceptable for critical failures.

using Zentient.Results;
using System;

public class CriticalOperation
{
    public IResult<ReportData> GenerateCriticalReport(int input)
    {
        if (input < 0)
        {
            return Result<ReportData>.BadRequest(new ErrorInfo(ErrorCategory.Validation, "InvalidInput", "Input cannot be negative."));
        }
        // Simulate complex report generation
        return Result<ReportData>.Success(new ReportData { Id = input });
    }

    public void ExecuteReportGeneration(int input)
    {
        try
        {
            // Use ThrowIfFailure() at the boundary
            ReportData data = GenerateCriticalReport(input).ThrowIfFailure().Value;
            Console.WriteLine($"Report {data.Id} generated successfully.");
        }
        catch (ResultException ex)
        {
            Console.Error.WriteLine($"Critical report generation failed: {ex.Message}");
            foreach (var error in ex.Errors)
            {
                Console.Error.WriteLine($"- Error ({error.Code}): {error.Message}");
            }
        }
        catch (Exception ex)
        {
            // Catch other unexpected exceptions
            Console.Error.WriteLine($"An unexpected error occurred: {ex.Message}");
        }
    }
}

public class ReportData { public int Id { get; set; } }

Using Match for Exhaustive Handling

The Match method provides a clean way to handle both success and failure branches, similar to pattern matching. It ensures you've considered both possibilities.

using Zentient.Results;
using System;

public class ProductFetcher
{
    public IResult<Product> GetProductById(string id)
    {
        if (id == "123") return Result<Product>.Success(new Product { Id = id, Name = "Laptop" });
        if (id == "404") return Result<Product>.NotFound(new ErrorInfo(ErrorCategory.NotFound, "ProductNotFound", "Product was not found."));
        return Result<Product>.BadRequest(new ErrorInfo(ErrorCategory.Validation, "InvalidId", "Invalid product ID format."));
    }

    public void DisplayProductDetails(string productId)
    {
        GetProductById(productId).Match(
            onSuccess: product =>
            {
                Console.WriteLine($"Product found: {product.Name} (ID: {product.Id})");
            },
            onFailure: errors =>
            {
                // errors is an IReadOnlyList<ErrorInfo>
                Console.WriteLine($"Failed to retrieve product '{productId}':");
                foreach (var error in errors)
                {
                    Console.WriteLine($"- {error.Message} (Code: {error.Code}, Category: {error.Category})");
                    // Can also check error.Status to get HTTP-aligned status
                }
            }
        );
    }
}

3. Working with Non-Generic Result

Use IResult and Result when an operation's primary outcome is just success or failure, without a specific return value.

using Zentient.Results;
using System;

public class DataDeleter
{
    public IResult DeleteRecord(Guid recordId)
    {
        if (recordId == Guid.Empty)
        {
            return Result.BadRequest(
                new ErrorInfo(ErrorCategory.Validation, "InvalidId", "Record ID cannot be empty.")
            );
        }

        // Simulate successful deletion
        if (recordId == new Guid("A0000000-0000-0000-0000-000000000001"))
        {
            Console.WriteLine($"Record {recordId} deleted successfully.");
            return Result.Success(ResultStatuses.NoContent); // Common for deletions
        }

        // Simulate not found
        return Result.NotFound(
            new ErrorInfo(ErrorCategory.NotFound, "RecordNotFound", $"Record {recordId} not found.")
        );
    }

    public void TryDeleteData()
    {
        IResult deleteResult = DeleteRecord(new Guid("A0000000-0000-0000-0000-000000000001"));
        if (deleteResult.IsSuccess)
        {
            Console.WriteLine("Deletion operation completed successfully.");
        }
        else
        {
            Console.WriteLine($"Deletion failed: {deleteResult.Error} (Status: {deleteResult.Status.Code})");
        }

        IResult invalidIdResult = DeleteRecord(Guid.Empty);
        if (invalidIdResult.IsFailure)
        {
            Console.WriteLine($"Invalid ID for deletion: {invalidIdResult.Error}");
        }
    }
}

4. Aggregating Errors (e.g., for Validation)

Zentient.Results allows you to return multiple errors within a single Result, particularly useful for input validation where many issues might be found at once.

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

public class RegistrationValidator
{
    public IResult ValidateRegistrationRequest(RegistrationRequest request)
    {
        var errors = new List<ErrorInfo>();

        if (string.IsNullOrWhiteSpace(request.Username) || request.Username.Length < 5)
        {
            errors.Add(new ErrorInfo(ErrorCategory.Validation, "UsernameLength", "Username must be at least 5 characters."));
        }
        if (string.IsNullOrWhiteSpace(request.Email) || !request.Email.Contains("@"))
        {
            errors.Add(new ErrorInfo(ErrorCategory.Validation, "InvalidEmail", "Email format is invalid."));
        }
        if (string.IsNullOrWhiteSpace(request.Password) || request.Password.Length < 8)
        {
            errors.Add(new ErrorInfo(ErrorCategory.Validation, "PasswordLength", "Password must be at least 8 characters."));
        }

        if (errors.Any()) // Use LINQ's Any()
        {
            // Create a failure result with all collected errors
            return Result.Validation(errors); // Convenience method for 400 validation error
        }

        return Result.Success();
    }
}

public class RegistrationRequest
{
    public string Username { get; set; }
    public string Email { get; set; }
    public string Password { get; set; }
}

5. Passing Contextual Data with ErrorInfo.Data

The ErrorInfo.Data property is an object? that allows you to attach any additional contextual information relevant to the error. This is powerful for debugging or providing rich client-side error details.

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

public class FileProcessingService
{
    public IResult ProcessFile(string fileName)
    {
        if (!System.IO.File.Exists(fileName))
        {
            return Result.NotFound(new ErrorInfo(
                ErrorCategory.NotFound,
                "FileNotFound",
                $"File '{fileName}' was not found.",
                new { RequestedPath = fileName, CurrentDirectory = Environment.CurrentDirectory } // Attach anonymous object as data
            ));
        }

        if (new System.IO.FileInfo(fileName).Length > 1024 * 1024) // 1MB
        {
            return Result.BadRequest(new ErrorInfo(
                ErrorCategory.Validation,
                "FileTooLarge",
                $"File '{fileName}' exceeds maximum allowed size.",
                new { MaxSizeMB = 1, ActualSizeKB = new System.IO.FileInfo(fileName).Length / 1024 } // Attach structured data
            ));
        }

        Console.WriteLine($"File '{fileName}' processed successfully.");
        return Result.Success();
    }
}

Next Steps

You've explored the core patterns for creating and consuming Zentient.Results. To learn more about specific aspects and advanced techniques, proceed to:

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