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");
}