Skip to content

Advanced

4.4.1. ✅ Applicative Validation — Result.Validate

Run multiple independent validations and accumulate all errors at once. Distinct from Bind (which short-circuits on first failure) and Combine (same-type collection, no mapper):

// All three validations run regardless of individual failure — ALL errors surface
Result<CreateOrderDto> dto = Result.Validate(
    ValidateName(request.Name),       // Result<string>
    ValidateEmail(request.Email),     // Result<string>
    ValidateAge(request.Age),         // Result<int>
    (name, email, age) => new CreateOrderDto(name, email, age));

// If Name and Age fail, dto.Errors contains BOTH errors simultaneously
// If all succeed, dto.Value = new CreateOrderDto(...)

2-way and 4-way overloads follow the same pattern. Mapper is only invoked when all inputs succeed.


4.4.2. 🔓 Tuple Unpacking — Result<T>.Deconstruct

C# 8+ deconstruction support for concise result handling:

// 2-component: value is default when IsFailure
var (value, errors) = GetUser(id);
if (errors.Count == 0) Console.WriteLine(value!.Name);

// 3-component: full unpack
var (isSuccess, value, errors) = GetUser(id);
if (isSuccess) Console.WriteLine(value!.Name);

// Non-generic Result
var (isSuccess, errors) = DoSomething();

4.4.3. 🔁 Maybe<T>Result<T> Interop

Bridge between the two optional-value types in the library:

// Maybe → Result (None becomes a typed failure)
Maybe<User> maybe = repository.FindUser(id);
Result<User> result = maybe.ToResult(() => new NotFoundError("User", id)); // lazy factory
Result<User> result = maybe.ToResult(new NotFoundError("User", id));      // static error
Result<User> result = maybe.ToResult("User not found");                    // string overload

// Result → Maybe (error info is discarded — use when absence, not error detail, is needed)
Maybe<User> maybe = result.ToMaybe(); // Some(user) on success, None on failure

4.4.4. ✅ Best Practices

Do: - Use Result<T> for expected business failures (validation, not found, conflict) - Create custom error types for your domain (OrderNotFoundError, InsufficientStockError) - Use tags to add structured context: .WithTag("OrderId", id).WithTag("StatusCode", 422) - Chain operations with Bind for sequential steps; Map for transforms only - Test both success and failure paths in unit tests

Avoid: - Using Result<T> for truly unexpected/exceptional cases — those still warrant exceptions - Accessing .Value without checking IsSuccess first (use GetValueOrDefault or Match) - Deep nesting — break complex pipelines into small named methods - Ignoring errors — always handle the failure case in Match

4.4.5. 🎯 When to Use Each Pattern

Pattern Best For When to Avoid
Maybe\ Optional values, cache lookups When you need error details
OneOf\ Typed multi-outcome returns, API responses When you have >6 outcomes
Result + LINQ Complex data pipelines with query syntax Simple single-step operations
Compose / Sequence Multi-step pipelines, fan-out/fan-in Single-step operations

4.4.6. 🔄 Functional Composition

Build complex operations from simple functions:

// Function composition
Func<CreateUserRequest, Result<User>> createUserPipeline = Compose(
    ValidateRequest,
    MapToUser,
    ValidateUser,
    SaveUser,
    SendWelcomeEmail
);

// Use the composed function
var result = createUserPipeline(request);

// Higher-order functions with Result
var results = users
    .Where(u => u.IsActive)
    .Select(u => ProcessUser(u))
    .Sequence(); // Turns IEnumerable<Result<T>> into Result<IEnumerable<T>>

// Async traverse operations
var results = await userIds
    .Traverse(id => GetUserAsync(id)); // Async version of Sequence

// Error aggregation
var aggregatedResult = results
    .Map(users => users.ToList())
    .Tap(users => LogInfo($"Processed {users.Count} users"));

4.4.7. 🚀 Performance Patterns

Optimize for high-performance scenarios:

// Value objects for reduced allocations
public readonly record struct UserEmail(string Value)
{
    public static Result<UserEmail> Create(string email) =>
        string.IsNullOrWhiteSpace(email)
            ? Result<UserEmail>.Fail("Email required")
            : email.Contains("@")
                ? Result<UserEmail>.Ok(new UserEmail(email))
                : Result<UserEmail>.Fail("Invalid email format");
}

// Array pooling for high-throughput scenarios
using System.Buffers;

var result = Result<string[]>.Ok(ArrayPool<string>.Shared.Rent(1000))
    .Ensure(arr => arr.Length >= 1000, "Array too small")
    .Tap(arr => ArrayPool<string>.Shared.Return(arr));

// Memory-efficient validation
public ref struct ValidationSpan(ReadOnlySpan<char> input)
{
    public bool IsValid => !input.IsEmpty && input.Contains('@');
    public Result<ReadOnlySpan<char>> AsResult() =>
        IsValid ? Result<ReadOnlySpan<char>>.Ok(input)
                : Result<ReadOnlySpan<char>>.Fail("Invalid email");
}