Skip to content

Latest commit

 

History

History
1301 lines (1061 loc) · 34.7 KB

features-results.md

File metadata and controls

1301 lines (1061 loc) · 34.7 KB

Results Feature Documentation

Overview

Challenges

When developing modern applications, handling operation outcomes effectively presents several challenges:

  1. Inconsistent Error Handling: Different parts of the application may handle errors in different ways, leading to inconsistent error reporting and handling.
  2. Context Loss: Important error context and details can be lost when exceptions are caught and rethrown up the call stack.
  3. Mixed Concerns: Business logic errors often get mixed with technical exceptions, making it harder to handle each appropriately.
  4. Pagination Complexity: Managing paginated data with associated metadata adds complexity to result handling.
  5. Type Safety: Maintaining type safety while handling both successful and failed operations can be challenging.
  6. Error Propagation: Propagating errors through multiple layers of the application while preserving context.

Solution

The Result pattern implementation provides a comprehensive solution by:

  1. Providing a standardized way to handle operation outcomes
  2. Encapsulating success/failure status, messages, and errors in a single object
  3. Supporting generic result types for operations that return values
  4. Offering specialized support for paginated results
  5. Enabling strongly-typed error handling
  6. Maintaining immutability with a fluent interface design

Architecture

A type-safe Result pattern implementation for explicit success/failure handling with optional functional extensions.

The Result pattern consists of three primary classes in hierarchy: Result provides base success/failure tracking with message and error collections, ResultT adds generic type support for strongly-typed value handling, and PagedResultT extends this for collection scenarios with pagination metadata. Each maintains a fluent interface with factory methods (Success(), Failure()). Error handling is supported through IResultError, enabling custom error types across the hierarchy.

The pattern can be enhanced with optional functional extensions like Map/Bind for transformations, Tap for side effects, and Filter/Unless for conditionals and more. See Appendix B: Functional Extensions for an overview of the available functional operations.

classDiagram
    class IResultError {
        <<interface>>
        +string Message
    }

    class ResultErrorBase {
        <<abstract>>
        +string Message
        +ResultErrorBase(string message)
    }

    class IResult {
        <<interface>>
        +IReadOnlyList Messages
        +IReadOnlyList Errors
        +bool IsSuccess
        +bool IsFailure
        +bool HasError()
    }

    class IResultT {
        <<interface>>
        +T Value
    }

    class Result {
        -List messages
        -List errors
        -bool success
        +bool IsSuccess
        +bool IsFailure
        +Result WithMessage(string)
        +Result WithMessages(IEnumerable)
        +Result WithError(IResultError)
        +Result WithErrors(IEnumerable)
        +Result For()
        +static Result Success()
        +static Result Failure()
    }

    class ResultT {
        -List messages
        -List errors
        -bool success
        +T Value
        +ResultT WithMessage(string)
        +ResultT WithMessages(IEnumerable)
        +ResultT WithError(IResultError)
        +ResultT WithErrors(IEnumerable)
        +Result For()
        +static ResultT Success(T value)
        +static ResultT Failure()
    }

    class PagedResultT {
        +int CurrentPage
        +int TotalPages
        +long TotalCount
        +int PageSize
        +bool HasPreviousPage
        +bool HasNextPage
        +static PagedResultT Success(IEnumerable, long, int, int)
        +static PagedResultT Failure()
    }

    IResultError <|.. ResultErrorBase
    IResult <|-- IResultT
    IResult <|.. Result
    IResultT <|.. ResultT
    ResultT <|-- PagedResultT
    Result ..> IResultError : uses
    ResultT ..> IResultError : uses
    PagedResultT ..> IResultError : uses
    IResult ..> IResultError : contains
Loading

Use Cases

The Result pattern is particularly useful in the following scenarios:

  1. Service Layer Operations
  • Handling business rule validations
  • Processing complex operations with multiple potential failure points
  • Returning domain-specific errors
  1. Data Access Operations
  • Managing database operations
  • Handling entity not found scenarios
  • Dealing with validation errors
  1. API Endpoints
  • Returning paginated data
  • Handling complex operation outcomes
  • Providing detailed error information
  1. Complex Workflows
  • Managing multi-step processes
  • Handling conditional operations
  • Aggregating errors from multiple sources

Usage

Basic Result Operations

// Creating success results
var success = Result.Success();
var successWithMessage = Result.Success("Operation completed successfully");

// Creating failure results
var failure = Result.Failure("Operation failed")
  .WithError<ValidationError>();

// Checking result status
if (success.IsSuccess)
{
    // Handle success
}

if (failure.IsFailure)
{
    // Handle failure
}

// Error handling
if (failure.HasError<ValidationError>())
{
    // Handle specific error type
}

// Paged result
var pagedResult = PagedResultT<Item>.Success(
    items, totalCount: 100, page: 1, pageSize: 10
);

Result with Values

public class UserService
{
    private readonly IUserRepository repository;

    public UserService(IUserRepository repository)
    {
        this.repository = repository;
    }

    public Result<User> GetUserById(int id)
    {
        var user = this.repository.FindById(id);
        if (user == null)
        {
            return Result<User>.Failure()
                .WithError<NotFoundResultError>()
                .WithMessage($"User {id} not found");
        }

        return Result<User>.Success(user);
    }
}

Error Handling

public class ValidationError : ResultErrorBase
{
    public ValidationError(string message) : base(message)
    {
    }
}

public class ValidationService
{
    public Result ValidateData(DataModel model)
    {
        var result = Result.Success();

        if (!this.IsValid(model))
        {
            result = Result.Failure()
                .WithError(new ValidationError("Invalid input"))
                .WithMessage("Validation failed");
        }

        // Check for specific errors
        if (result.HasError<ValidationError>())
        {
            // Handle validation error
        }

        if (result.HasError<ValidationError>(out var validationErrors))
        {
            foreach (var error in validationErrors)
            {
                Console.WriteLine(error.Message);
            }
        }

        return result;
    }

    private bool IsValid(DataModel model)
    {
        // Validation logic
        return true;
    }
}

Working with Messages

public class WorkflowService
{
    public Result ProcessWorkflow()
    {
        var result = Result.Success()
            .WithMessage("Step 1 completed")
            .WithMessage("Step 2 completed");

        var messages = new[] { "Process started", "Process completed" };
        result = Result.Success()
            .WithMessages(messages);

        foreach (var message in result.Messages)
        {
            Console.WriteLine(message);
        }

        return result;
    }
}

Paged Results

public class ProductService
{
    private readonly IProductRepository repository;
    private readonly ILogger<ProductService> logger;

    public ProductService(
        IProductRepository repository,
        ILogger<ProductService> logger)
    {
        this.repository = repository;
        this.logger = logger;
    }

    public async Task<PagedResult<Product>> GetProductsAsync(int page = 1, int pageSize = 10)
    {
        try
        {
            var totalCount = await this.repository.CountAsync();
            var products = await this.repository.GetPageAsync(page, pageSize);

            return PagedResult<Product>.Success(
                products,
                totalCount,
                page,
                pageSize);
        }
        catch (Exception ex)
        {
            this.logger.LogError(ex, "Failed to get products");
            return PagedResult<Product>.Failure()
                .WithError(new ExceptionError(ex));
        }
    }
}

Custom Error Types

public class ValidationResultError : ResultErrorBase
{
    public ValidationResultError(string field, string message)
        : base($"Validation failed for {field}: {message}")
    {
    }
}

public class UnauthorizedResultError : ResultErrorBase
{
    public UnauthorizedResultError()
        : base("User is not authorized to perform this action")
    {
    }
}

public class OrderService
{
    private readonly IAuthService authService;
    private readonly IOrderRepository orderRepository;

    public OrderService(
        IAuthService authService,
        IOrderRepository orderRepository)
    {
        this.authService = authService;
        this.orderRepository = orderRepository;
    }

    public Result<Order> CreateOrder(OrderRequest request)
    {
        if (!this.authService.CanCreateOrders())
        {
            return Result<Order>.Failure<UnauthorizedResultError>();
        }

        if (request.Quantity <= 0)
        {
            return Result<Order>.Failure()
                .WithError(new ValidationResultError("Quantity", "Must be greater than zero"));
        }

        var order = this.orderRepository.Create(request);
        return Result<Order>.Success(order, "Order created successfully");
    }
}

Exception Handling

public class DataService
{
    private readonly IDataRepository repository;
    private readonly ILogger<DataService> logger;

    public DataService(
        IDataRepository repository,
        ILogger<DataService> logger)
    {
        this.repository = repository;
        this.logger = logger;
    }

    public Result<Data> GetData()
    {
        try
        {
            var data = this.repository.FetchData();
            return Result<Data>.Success(data);
        }
        catch (Exception ex)
        {
            this.logger.LogError(ex, "Failed to fetch data");
            return Result<Data>.Failure()
                .WithError(new ExceptionError(ex))
                .WithMessage("Failed to fetch data");
        }
    }
}

Best Practices

  1. Early Returns: Return failures as soon as possible to avoid unnecessary processing.
public class OrderProcessor
{
    public Result<Order> ProcessOrder(OrderRequest request)
    {
        if (request == null)
        {
            return Result<Order>.Failure("Request cannot be null");
        }

        if (!request.IsValid)
        {
            return Result<Order>.Failure("Invalid request");
        }

        // Continue processing
        return Result<Order>.Success(new Order(request));
    }
}
  1. Meaningful Messages: Include context in error messages.
public Result ProcessOrderById(int orderId, string status)
{
    return Result.Failure($"Failed to process order {orderId}: Invalid status {status}");
}
  1. Type-Safe Errors: Use strongly-typed errors for better error handling.
public class OrderNotFoundError : ResultErrorBase
{
    public OrderNotFoundError(int orderId)
        : base($"Order {orderId} not found")
    {
    }
}

Examples

Repository Pattern Example

public class UserRepository
{
    private readonly DbContext dbContext;

    public UserRepository(DbContext dbContext)
    {
        this.dbContext = dbContext;
    }

    public Result<User> GetById(int id)
    {
        try
        {
            var user = this.dbContext.Users.FindById(id);
            if (user == null)
            {
                return Result<User>.Failure<NotFoundResultError>();
            }

            return Result<User>.Success(user);
        }
        catch (Exception ex)
        {
            return Result<User>.Failure()
                .WithError(new ExceptionError(ex))
                .WithMessage($"Failed to retrieve user {id}");
        }
    }
}

Service Layer Example

public class UserService
{
    private readonly IUserRepository repository;
    private readonly IValidator<User> validator;
    private readonly IMapper mapper;

    public UserService(
        IUserRepository repository,
        IValidator<User> validator,
        IMapper mapper)
    {
        this.repository = repository;
        this.validator = validator;
        this.mapper = mapper;
    }

    public Result<UserDto> UpdateUser(int id, UpdateUserRequest request)
    {
        var getUserResult = this.repository.GetById(id);
        if (getUserResult.IsFailure)
        {
            return Result<UserDto>.Failure()
                .WithErrors(getUserResult.Errors)
                .WithMessages(getUserResult.Messages);
        }

        var user = getUserResult.Value;
        user.Update(request);

        var validationResult = this.validator.Validate(user);
        if (!validationResult.IsValid)
        {
            return Result<UserDto>.Failure()
                .WithError(new ValidationResultError("User", validationResult.Error));
        }

        var savedUser = this.repository.Save(user);
        return Result<UserDto>.Success(this.mapper.ToDto(savedUser));
    }
}

API Controller Example

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductService productService;

    public ProductsController(IProductService productService)
    {
        this.productService = productService;
    }

    [HttpGet]
    public async Task<IActionResult> GetProducts([FromQuery] int page = 1, [FromQuery] int pageSize = 10)
    {
        var result = await this.productService.GetProductsAsync(page, pageSize);

        if (result.IsFailure)
        {
            return this.BadRequest(new
            {
                Errors = result.Errors.Select(e => e.Message),
                Messages = result.Messages
            });
        }

        return this.Ok(new
        {
            Data = result.Value,
            Pagination = new
            {
                result.CurrentPage,
                result.TotalPages,
                result.TotalCount,
                result.HasNextPage,
                result.HasPreviousPage
            }
        });
    }
}

Complex Workflow Example

public class OrderProcessor
{
    private readonly IOrderRepository orderRepository;
    private readonly IInventoryService inventoryService;
    private readonly IPaymentService paymentService;
    private readonly INotificationService notificationService;
    private readonly IValidator<OrderRequest> validator;
    private readonly ILogger<OrderProcessor> logger;

    public OrderProcessor(
        IOrderRepository orderRepository,
        IInventoryService inventoryService,
        IPaymentService paymentService,
        INotificationService notificationService,
        IValidator<OrderRequest> validator,
        ILogger<OrderProcessor> logger)
    {
        this.orderRepository = orderRepository;
        this.inventoryService = inventoryService;
        this.paymentService = paymentService;
        this.notificationService = notificationService;
        this.validator = validator;
        this.logger = logger;
    }

    public async Task<Result<Order>> ProcessOrderAsync(OrderRequest request)
    {
        // Validate request
        var validationResult = await this.ValidateOrderRequestAsync(request);
        if (validationResult.IsFailure)
        {
            return Result<Order>.Failure()
                .WithErrors(validationResult.Errors)
                .WithMessage("Order validation failed");
        }

        // Reserve inventory
        var inventoryResult = await this.ReserveInventoryAsync(request.Items);
        if (inventoryResult.IsFailure)
        {
            return Result<Order>.Failure()
                .WithErrors(inventoryResult.Errors)
                .WithMessage("Inventory reservation failed");
        }

        try
        {
            // Process payment
            var paymentResult = await this.ProcessPaymentAsync(request.Payment);
            if (paymentResult.IsFailure)
            {
                // Rollback inventory reservation
                await this.ReleaseInventoryAsync(request.Items);
                return Result<Order>.Failure()
                    .WithErrors(paymentResult.Errors)
                    .WithMessage("Payment processing failed");
            }

            // Create order
            var order = await this.CreateOrderAsync(request, paymentResult.Value);
            if (order.IsFailure)
            {
                // Rollback payment and inventory
                await this.ReversePaymentAsync(paymentResult.Value);
                await this.ReleaseInventoryAsync(request.Items);
                return Result<Order>.Failure()
                    .WithErrors(order.Errors)
                    .WithMessage("Order creation failed");
            }

            // Send notifications
            await this.notificationService.SendOrderConfirmationAsync(order.Value);

            return Result<Order>.Success(order.Value)
                .WithMessage("Order processed successfully");
        }
        catch (Exception ex)
        {
            this.logger.LogError(ex, "Unexpected error during order processing");
            return Result<Order>.Failure()
                .WithError(new ExceptionError(ex))
                .WithMessage("An unexpected error occurred while processing the order");
        }
    }

    private async Task<Result> ValidateOrderRequestAsync(OrderRequest request)
    {
        try
        {
            var validationResult = await this.validator.ValidateAsync(request);
            if (!validationResult.IsValid)
            {
                return Result.Failure()
                    .WithError(new ValidationResultError("Order", validationResult.Message));
            }

            return Result.Success();
        }
        catch (Exception ex)
        {
            this.logger.LogError(ex, "Validation error");
            return Result.Failure()
                .WithError(new ExceptionError(ex));
        }
    }

    private async Task<Result<InventoryReservation>> ReserveInventoryAsync(IEnumerable<OrderItem> items)
    {
        try
        {
            return await this.inventoryService.ReserveItemsAsync(items);
        }
        catch (Exception ex)
        {
            this.logger.LogError(ex, "Inventory reservation error");
            return Result<InventoryReservation>.Failure()
                .WithError(new ExceptionError(ex));
        }
    }

    private async Task<Result<PaymentTransaction>> ProcessPaymentAsync(PaymentDetails payment)
    {
        try
        {
            var transaction = await this.paymentService.ProcessAsync(payment);
            if (transaction.IsFailure)
            {
                this.logger.LogWarning("Payment failed: {Message}", transaction.Messages.FirstOrDefault());
            }

            return transaction;
        }
        catch (Exception ex)
        {
            this.logger.LogError(ex, "Payment processing error");
            return Result<PaymentTransaction>.Failure()
                .WithError(new ExceptionError(ex));
        }
    }

    private async Task<Result<Order>> CreateOrderAsync(OrderRequest request, PaymentTransaction transaction)
    {
        try
        {
            var order = new Order(request, transaction);
            return await this.orderRepository.InsertResultAsync(order);
        }
        catch (Exception ex)
        {
            this.logger.LogError(ex, "Order creation error");
            return Result<Order>.Failure()
                .WithError(new ExceptionError(ex));
        }
    }

    private async Task ReleaseInventoryAsync(IEnumerable<OrderItem> items)
    {
        try
        {
            await this.inventoryService.ReleaseReservationAsync(items);
        }
        catch (Exception ex)
        {
            this.logger.LogError(ex, "Error releasing inventory");
        }
    }

    private async Task ReversePaymentAsync(PaymentTransaction transaction)
    {
        try
        {
            await this.paymentService.ReverseTransactionAsync(transaction);
        }
        catch (Exception ex)
        {
            this.logger.LogError(ex, "Error reversing payment");
        }
    }
}

This example demonstrates:

  1. Proper Error Handling
  • Each operation returns a Result or Result<T>
  • Errors are properly propagated and transformed
  • Rollback operations are performed when needed
  1. Clean Code Practices
  • Follows the .editorconfig standards
  • Proper use of dependency injection
  • Clear separation of concerns
  • Consistent error handling
  1. Workflow Management
  • Sequential processing with proper validation
  • Rollback mechanisms for failed operations
  • Proper logging at each step
  1. Result Pattern Usage
  • Consistent use of Result objects
  • Proper error aggregation
  • Clear success/failure paths
  1. Resource Management
  • Proper cleanup in case of failures
  • Structured error handling
  • Comprehensive logging

Appendix A: Repository Extensions

The DevKit provides extension methods for repositories that don't natively support the Result pattern. These extensions wrap standard repository operations in Result objects, providing consistent error handling and operation results across your application.

Available Extensions

  1. Read-Only Repository Extensions (GenericReadOnlyRepositoryResultExtensions):
  • Count operations
  • Find operations
  • Paged query operations
  1. Repository Extensions (GenericRepositoryResultExtensions):
  • Insert operations
  • Update operations
  • Upsert operations
  • Delete operations

Usage Examples

Basic CRUD Operations

public class UserService
{
    private readonly IGenericRepository<User> _repository;

    // Insert with Result
    public async Task<Result<User>> CreateUserAsync(User user)
    {
        return await _repository.InsertResultAsync(user);
    }

    // Update with Result
    public async Task<Result<User>> UpdateUserAsync(User user)
    {
        return await _repository.UpdateResultAsync(user);
    }

    // Delete with Result
    public async Task<Result<RepositoryActionResult>> DeleteUserAsync(int id)
    {
        return await _repository.DeleteByIdResultAsync(id);
    }
}

Query Operations

public class ProductService
{
    private readonly IGenericReadOnlyRepository<Product> _repository;

    // Count with Result
    public async Task<Result<long>> GetProductCountAsync()
    {
        return await _repository.CountResultAsync();
    }

    // Find One with Result
    public async Task<Result<Product>> GetProductByIdAsync(int id)
    {
        return await _repository.FindOneResultAsync(id);
    }

    // Find All Paged with Result
    public async Task<PagedResult<Product>> GetProductsPagedAsync(
        int page = 1,
        int pageSize = 10)
    {
        return await _repository.FindAllPagedResultAsync(
            ordering: "Name ascending",
            page: page,
            pageSize: pageSize);
    }
}

Advanced Queries

public class OrderService
{
    private readonly IGenericReadOnlyRepository<Order> _repository;

    // Complex query with specifications
    public async Task<PagedResult<Order>> GetOrdersAsync(
        FilterModel filterModel,
        IEnumerable<ISpecification<Order>> additionalSpecs = null)
    {
        return await _repository.FindAllPagedResultAsync(
            filterModel,
            additionalSpecs);
    }

    // Query with includes
    public async Task<Result<Order>> GetOrderWithDetailsAsync(int id)
    {
        return await _repository.FindOneResultAsync(
            id,
            options: new FindOptions<Order>
            {
                Include = new IncludeOption<Order>(o => o.OrderItems)
            });
    }
}

Error Handling

The extensions automatically handle exceptions and wrap them in Result objects:

public class InventoryService
{
    private readonly IGenericRepository<Inventory> _repository;

    public async Task<Result<Inventory>> UpdateInventoryAsync(Inventory inventory)
    {
        var result = await _repository.UpdateResultAsync(inventory);

        if (result.IsFailure)
        {
            // Check for specific errors
            if (result.HasError<ExceptionError>())
            {
                // Handle database exception
                _logger.LogError(result.Errors.First().Message);
            }
        }

        return result;
    }
}

Best Practices

  1. Consistent Usage: Use these extensions throughout the application for consistent error handling:
// Instead of:
try {
    var product = await _repository.FindOneAsync(id);
    return product;
}
catch (Exception ex) {
    // Handle error
}

// Use:
var result = await _repository.FindOneResultAsync(id);
return result;
  1. Combining Operations: Chain repository operations while maintaining error handling:
public async Task<Result<Order>> ProcessOrderAsync(Order order)
{
    // Check inventory
    var inventoryResult = await _inventoryRepository
        .FindOneResultAsync(order.ProductId);

    if (inventoryResult.IsFailure)
        return Result<Order>.Failure()
            .WithErrors(inventoryResult.Errors);

    // Insert order
    var orderResult = await _orderRepository
        .InsertResultAsync(order);

    return orderResult;
}
  1. Paged Queries with Specifications:
public async Task<PagedResult<Product>> SearchProductsAsync(
    string searchTerm,
    int page = 1,
    int pageSize = 10)
{
    var specification = new Specification<Product>(p => p.Name.Contains(searchTerm));

    return await _repository.FindAllPagedResultAsync(specification);
}
  1. Filtering with model:
public async Task<PagedResult<Order>> GetOrdersAsync(FilterModel filterModel)
{
    var specifications = new List<ISpecification<Order>>
    {
        new Specification<Order>(o => o.Status == OrderStatus.Active)
    };

    return await _repository.FindAllPagedResultAsync(filterModel, specifications);
}

These extensions provide a seamless way to integrate the Result pattern with existing repository implementations, ensuring consistent error handling and operation results across your application.

Appendix B: Functional Extensions

Composable, type-safe operations for elegant Result error handling and flow control.

The functional programming extensions transform the Result pattern from a simple success/failure container into a powerful composition tool. By providing fluent, chainable operations like Map, Bind, and Match, complex workflows can be expressed as a series of small, focused transformations. This approach eliminates nested error handling, reduces complexity, and makes the code's intent clearer.

Each operation maintains the Result context, automatically handling error propagation and ensuring type safety throughout the chain. The pattern enables both synchronous and asynchronous operations, side effects, and validations while keeping the core business logic clean and maintainable. This functional style particularly shines in handling complex flows where multiple operations must be composed together, each potentially failing, with proper error context preserved throughout the chain.

Core Operations

Essential value transformations and validations ensuring Result integrity.

  • Map: Transform success value into different type
await result.MapAsync(async (user, ct) => await LoadUserPreferencesAsync(user));
  • Bind: Chain Results together, preserving errors
await result.BindAsync(async (user, ct) => await ValidateAndTransformUserAsync(user));
  • Ensure: Verify condition on success value
await result.EnsureAsync(
    async (user, ct) => await CheckUserStatusAsync(user),
    new Error("Invalid status"));
  • Try: Wrap async operation in Result handling exceptions
await Result<User>.TryAsync(async ct => await repository.GetUserAsync(userId, ct));
  • Validate: Validate collection of values using FluentValidation
await result.ValidateAsync(new UserValidator(),
    strategy => strategy.IncludeRuleSets("Create"));

Side Effects

Execute operations without changing Result value.

  • Tap: Execute side effect on success value
await result.TapAsync(async (user, ct) => await _cache.StoreUserAsync(user));
  • TeeMap: Transform and execute side effect
await result.TeeMapAsync(
    user => user.ToDto(),
    async (dto, ct) => await _cache.StoreDtoAsync(dto));
  • Do: Execute action regardless of Result state
await result.DoAsync(async ct => await InitializeSystemAsync(ct));
  • AndThen: Chain operations preserving original value
await result.AndThenAsync(async (user, ct) => await ValidateUserAsync(user));

Control Flow

Conditional logic and alternative paths.

  • Filter: Convert to failure if predicate fails
await result.FilterAsync(
    async (user, ct) => await HasValidSubscriptionAsync(user),
    new SubscriptionError("Invalid subscription"));
  • Unless: Convert to failure if predicate succeeds
await result.UnlessAsync(
    async (user, ct) => await IsBlacklistedAsync(user),
    new BlacklistError("Blacklisted"));
  • OrElse: Provide fallback value on failure
await result.OrElseAsync(
    async ct => await LoadDefaultUserAsync());
  • Switch: Execute conditional side effect
await result.SwitchAsync(
    user => user.IsAdmin,
    async (user, ct) => await NotifyAdminLoginAsync(user));

Transformations

Value mappings and collection handling.

  • BiMap: Transform both success and failure cases
await result.BiMap(
    user => new UserDto(user),
    errors => errors.Select(e => new PublicError(e)));
  • Choose: Filter optional values
await result.ChooseAsync(async (user, ct) =>
    user.IsActive ? Option<UserDto>.Some(new UserDto(user)) : Option<UserDto>.None());
  • Collect: Transform collection preserving all errors
await result.CollectAsync(async (user, ct) => await ValidateUserAsync(user));

Pattern Matching

Success/failure case handling.

  • Match: Handle success/failure with async functions
await result.MatchAsync(
    async (user, ct) => await ProcessUserAsync(user),
    async (errors, ct) => await HandleErrorsAsync(errors));
  • MatchAsync (mixed): Handle with mix of sync/async functions
await result.MatchAsync(
    async (user, ct) => await ProcessUserAsync(user),
    errors => "Processing failed");
  • Match (values): Return different values for success/failure
await result.Match(
    success: "Valid",
    failure: "Invalid");

Usage Example

Clean validation, external integration and transformation flow with automatic error propagation.

The chain processes a list of persons through a series of validations and transformations. Starting with input validation (age checks, location requirements), it transforms the data (email normalization), interacts with external services (email notifications), and performs final validations (database checks). Each operation in the chain either transforms the data or validates it, with errors propagating automatically through the chain (ResultT).

The functional style ensures that if any step fails, subsequent operations are skipped and the error context is preserved. The chain concludes by matching the result to either a success or failure message.

flowchart TB
    Input[Result List<Person>] --> A

    subgraph "Initial Validation"
        A[Do - Log Start] --> B[Validate persons]
        B --> C[Ensure age >= 18]
        C --> E[Filter locations]
    end

    subgraph "Data Transformation"
        E --> F[Map emails lowercase]
    end

    subgraph "External Operations"
        F --> H[AndThenAsync send emails]
    end

    subgraph "Final Validation"
        H --> K[EnsureAsync DB check]
    end

    K --> Final[Match result to string]

    %% Side Effects
    subgraph "Side Effects System"
        direction LR
        Email[Email Service]
        DB[Database]
    end

    H -.-> Email
    K -.-> DB
Loading
var people = new List<Person>{ personA, personB };
var result = await Result<List>.Success(people)
    .Do(() => logger.LogInformation("Starting person processing"))
    .Validate(validator)
    .Ensure(
        persons => persons.All(p => p.Age >= 18),
        new Error("All persons must be adults"))
    .Filter(
        persons => persons.All(p => p.Locations.Any()),
        new Error("All persons must have at least one location"))
    .Map(persons => persons.Select(p => {
        var email = EmailAddress.Create(p.Email.Value.ToLowerInvariant());
        return new Person(p.FirstName, p.LastName, email, p.Age, p.Locations);
    }).ToList())
    .AndThenAsync(async (persons, ct) =>
        await emailService.SendWelcomeEmailAsync(persons),
        CancellationToken.None)
    .EnsureAsync(async (persons, ct) =>
        await database.PersonsExistAsync(persons),
        new Error("Not all persons were saved correctly"))
    .Match(
        list => $"Successfully processed {list.Count} persons",
        errors => $"Processing failed: {string.Join(", ", errors)}"
    );