Skip to content

Core Operations

4.1.1. 💡 Why Result Instead of Exceptions?

❌ Exceptions ✅ Result Pattern
Visibility Hidden in method signatures Explicit in return type
Surprises Runtime surprises Compile-time guarantees
Code Complex try-catch blocks Simple pattern matching
Testing Hard to test error paths Easy to test both paths
Composition Break the call chain Chain operations cleanly

4.1.2. 🧠 Functional Programming Foundation

Railway-Oriented Programming (ROP) - Immutable Results: Thread-safe functional data structures - Error Composition: Chain operations without exception handling - Success/Failure Pipelines: Clean separation of happy and error paths - Type Safety: Compile-time guarantees for error handling

4.1.3. 🔧 Complete Method Catalog

4.1.3.1. Core Operations
// Factory Methods
Result<T>.Ok(value)                    // Success result
Result<T>.Fail("error")                 // Failure result
Result.Fail("error")                    // Non-generic failure

// Pattern Matching
result.Match(
    onSuccess: value => DoSomething(value),
    onFailure: errors => HandleErrors(errors)
);

// Value Access
result.Value                            // Throws if failed
result.GetValueOrDefault(defaultValue)  // Safe access
4.1.3.2. Functional Composition
// Bind (Chain operations)
var result = Result<int>.Ok(5)
    .Bind(x => Result<string>.Ok(x.ToString()))
    .Bind(s => ValidateEmail(s));

// Map (Transform success values)
var result = Result<int>.Ok(5)
    .Map(x => x * 2)
    .Map(x => x.ToString());

// Tap (Side effects without changing result)
var result = Result<User>.Ok(user)
    .Tap(u => LogUserAccess(u))
    .Tap(u => SendNotification(u));

// Ensure (Validation)
var result = Result<string>.Ok(email)
    .Ensure(e => IsValidEmail(e), "Invalid email format")
    .EnsureAsync(async e => !await EmailExistsAsync(e), "Email already registered");
4.1.3.3. Async Operations
// All methods have async variants
var result = await Result<int>.Ok(id)
    .BindAsync(async i => await GetUserAsync(i))
    .MapAsync(async user => await ToDtoAsync(user))
    .TapAsync(async dto => await LogAccessAsync(dto))
    .EnsureAsync(async dto => await ValidateDtoAsync(dto), "Invalid DTO");
4.1.3.4. Async Patterns (WhenAll, Retry, Timeout)
// Run multiple async results concurrently — typed tuples!
var result = await Result.WhenAll(GetUser(id), GetAccount(id));
var (user, account) = result.Value;

// Retry with exponential backoff
var result = await Result.Retry(
    () => CallExternalApi(),
    maxRetries: 3,
    delay: TimeSpan.FromSeconds(1),
    backoffFactor: 2.0);

// Enforce time limits
var result = await GetSlowData().Timeout(TimeSpan.FromSeconds(5));

4.1.4. 📊 LINQ Integration

Functional Query Comprehensions

// LINQ-like syntax for Result operations
var result = from user in GetUser(id)
            from validation in ValidateUser(user)
            from saved in SaveUser(validation)
            from notification in SendNotification(saved)
            select saved;

// Complex queries
var results = from id in userIds
             from user in GetUserAsync(id)
             from updated in UpdateUserAsync(user)
             select updated;

// Equivalent to method chaining
var result = GetUser(id)
    .Bind(ValidateUser)
    .Bind(SaveUser)
    .Bind(SendNotification);

4.1.5. 🎯 Advanced Patterns

4.1.5.1. Maybe - Null-Safe Optionals
// Instead of null references
Maybe<User> user = GetUserFromCache(id);
var email = user
    .Select(u => u.Email)
    .Filter(email => email.Contains("@"))
    .ValueOrDefault("no-reply@example.com");

// Safe operations
var result = user
    .Map(u => u.Name)
    .Bind(name => ValidateName(name))
    .ToResult(() => new UserNotFoundError(id));
4.1.5.2. OneOf - Discriminated Unions
// Internal OneOf implementation
OneOf<ValidationError, User> result = ValidateAndCreateUser(request);
return result.Match(
    case1: error => BadRequest(error),
    case2: user => Ok(user)
);

// Three-type OneOf
OneOf<ValidationError, NotFoundError, User> GetUser(int id) { /* logic */ }

// Conversion to Result
var result = oneOf.ToResult(); // Convert OneOf to Result
4.1.5.3. Validation Rules Framework
// Built-in validation
var validator = Validator.Create<User>()
    .Rule(u => u.Email, email => email.Contains("@"))
    .Rule(u => u.Name, name => !string.IsNullOrWhiteSpace(name))
    .Rule(u => u.Age, age => age >= 18, "Must be 18 or older");

var result = validator.Validate(user);
4.1.5.4. JSON Serialization (System.Text.Json)
using REslava.Result.Serialization;

// Register converters once
var options = new JsonSerializerOptions();
options.AddREslavaResultConverters();

// Result<T> serialization
var result = Result<User>.Ok(new User("Alice", "alice@test.com"));
var json = JsonSerializer.Serialize(result, options);
// {"isSuccess":true,"value":{"name":"Alice","email":"alice@test.com"},"errors":[],"successes":[]}

var deserialized = JsonSerializer.Deserialize<Result<User>>(json, options);

// OneOf<T1,T2> serialization
OneOf<Error, User> oneOf = OneOf<Error, User>.FromT2(user);
var json2 = JsonSerializer.Serialize(oneOf, options);
// {"index":1,"value":{"name":"Alice","email":"alice@test.com"}}

// Maybe<T> serialization
var maybe = Maybe<string>.Some("hello");
var json3 = JsonSerializer.Serialize(maybe, options);
// {"hasValue":true,"value":"hello"}

4.1.6. 🔧 CRTP Pattern & Method Chaining

Curiously Recurring Template Pattern

// Fluent method chaining with CRTP
var result = Result<User>.Ok(user)
    .Ensure(ValidateEmail)
    .Map(ToDto)
    .Tap(SendWelcomeEmail)
    .Bind(SaveToDatabase)
    .WithSuccess("User created successfully")
    .WithTag("UserId", user.Id);

4.1.7. 🔄 Advanced Extensions

Functional Composition

// Function composition
var createUser = Compose(
    ValidateRequest,
    MapToUser,
    SaveUser,
    SendNotification
);

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

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

4.1.8. 🚫 Tap on Failure — TapOnFailure

Execute a side-effect only when the result has failed — logging, metrics, alerting. The result passes through unchanged:

Result<User> result = await GetUserAsync(id)
    .TapOnFailure(error => _logger.LogWarning("GetUser failed: {Msg}", error.Message));

// Async side-effect
Result<Order> order = await CreateOrderAsync(request)
    .TapOnFailureAsync(async error => await _metrics.IncrementAsync("order.failed"));

// Non-generic Result
Result operation = await DeleteUserAsync(id)
    .TapOnFailure(error => _audit.RecordFailure("delete_user", error));

Combine with Tap() for full success/failure observability in one chain:

Result<Order> order = await CreateOrderAsync(request)
    .Tap(o => _logger.LogInformation("Order {Id} created", o.Id))
    .TapOnFailure(e => _logger.LogError("Order creation failed: {Msg}", e.Message));

4.1.9. 🔀 Conditional Factories — OkIf / FailIf

Create results directly from boolean conditions — no if/else boilerplate:

// Result (no value)
Result r1 = Result.OkIf(age >= 18, "Must be 18 or older");
Result r2 = Result.FailIf(user.IsSuspended, new ValidationError("Account suspended"));

// Result<T> — value on success
Result<User> r3 = Result<User>.OkIf(user != null, user!, "User not found");
Result<User> r4 = Result<User>.FailIf(user.Age < 18, new ValidationError("Age", "Must be 18+"), user);

// Lazy evaluation — condition and/or value computed only when needed
Result<User> r5 = Result<User>.OkIf(
    () => _db.IsUserActive(id),         // predicate evaluated lazily
    () => _db.LoadUser(id),             // value only loaded when needed
    "User not found");

// Async versions
Result result = await Result.OkIfAsync(() => _api.CheckExistsAsync(id), "Not found");
Result<User> result = await Result<User>.OkIfAsync(
    () => _api.CheckExistsAsync(id),
    () => _api.LoadUserAsync(id),
    "User not found");