Skip to content

Migration Guide

Overview

This guide helps you migrate your existing code to use TinyResult effectively. It covers common scenarios and provides step-by-step instructions for migration.

From Traditional Error Handling

1. Try-Catch Blocks

// Before: Traditional try-catch
public User GetUser(int id)
{
    try
    {
        var user = _repository.GetUser(id);
        if (user == null)
            throw new UserNotFoundException($"User {id} not found");
        return user;
    }
    catch (Exception ex)
    {
        _logger.Error(ex, "Error getting user");
        throw;
    }
}

// After: Using Result
public Result<User> GetUser(int id)
{
    return ResultPipeline<User>
        .Start(id)
        .Then(id => _repository.GetUser(id))
        .Validate(user => user != null, ErrorCode.NotFound, $"User {id} not found")
        .Catch(error => 
        {
            _logger.Error(error.Message);
            return Result<User>.Failure(error);
        })
        .Build();
}

2. Null Checks

// Before: Null checks with exceptions
public string ProcessData(string data)
{
    if (data == null)
        throw new ArgumentNullException(nameof(data));

    return data.ToUpper();
}

// After: Using Result
public Result<string> ProcessData(string data)
{
    return ResultPipeline<string>
        .Start(data)
        .Validate(d => d != null, ErrorCode.ValidationError, "Data cannot be null")
        .Map(d => d.ToUpper())
        .Build();
}

From Other Result Libraries

1. FluentResults

// Before: FluentResults
public Result<User> GetUser(int id)
{
    return Result.Ok(id)
        .Bind(id => _repository.GetUser(id))
        .Ensure(user => user != null, "User not found")
        .OnSuccess(user => _logger.Info($"User found: {user.Id}"))
        .OnFailure(error => _logger.Error(error.Message));
}

// After: TinyResult
public Result<User> GetUser(int id)
{
    return ResultPipeline<User>
        .Start(id)
        .Then(id => _repository.GetUser(id))
        .Validate(user => user != null, ErrorCode.NotFound, "User not found")
        .OnSuccess(user => _logger.Info($"User found: {user.Id}"))
        .OnFailure(error => _logger.Error(error.Message))
        .Build();
}

2. LanguageExt

// Before: LanguageExt
public Either<Error, User> GetUser(int id)
{
    return from user in _repository.GetUser(id)
           from validated in ValidateUser(user)
           select validated;
}

// After: TinyResult
public Result<User> GetUser(int id)
{
    return ResultPipeline<User>
        .Start(id)
        .Then(id => _repository.GetUser(id))
        .Then(user => ValidateUser(user))
        .Build();
}

From Custom Result Types

1. Simple Result

// Before: Custom Result
public class OperationResult<T>
{
    public bool Success { get; }
    public T Value { get; }
    public string ErrorMessage { get; }
}

public OperationResult<User> GetUser(int id)
{
    var user = _repository.GetUser(id);
    if (user == null)
        return OperationResult<User>.Failure("User not found");
    return OperationResult<User>.Success(user);
}

// After: TinyResult
public Result<User> GetUser(int id)
{
    return ResultPipeline<User>
        .Start(id)
        .Then(id => _repository.GetUser(id))
        .Validate(user => user != null, ErrorCode.NotFound, "User not found")
        .Build();
}

2. Result with Metadata

// Before: Custom Result with Metadata
public class OperationResult<T>
{
    public bool Success { get; }
    public T Value { get; }
    public string ErrorMessage { get; }
    public Dictionary<string, object> Metadata { get; }
}

// After: TinyResult
public Result<User> GetUser(int id)
{
    return ResultPipeline<User>
        .Start(id)
        .Then(id => _repository.GetUser(id))
        .Validate(user => user != null, ErrorCode.NotFound, "User not found")
        .OnSuccess(user => 
        {
            user.Metadata["RetrievedAt"] = DateTime.UtcNow;
            return user;
        })
        .Build();
}

Common Migration Patterns

1. Converting Exceptions

// Before: Exception-based
public User GetUser(int id)
{
    try
    {
        return _repository.GetUser(id);
    }
    catch (SqlException ex)
    {
        throw new DatabaseException("Database error", ex);
    }
}

// After: Result-based
public Result<User> GetUser(int id)
{
    return Result.FromTry(
        () => _repository.GetUser(id),
        ex => ex is SqlException
            ? Error.Create(ErrorCode.DatabaseError, "Database error", ex)
            : Error.Create(ErrorCode.InternalError, "Unexpected error", ex)
    );
}

2. Handling Multiple Results

// Before: Multiple checks
public (User User, Order Order) GetUserAndOrder(int userId, int orderId)
{
    var user = _userRepository.GetUser(userId);
    if (user == null)
        throw new UserNotFoundException($"User {userId} not found");

    var order = _orderRepository.GetOrder(orderId);
    if (order == null)
        throw new OrderNotFoundException($"Order {orderId} not found");

    return (user, order);
}

// After: Using Result.Combine
public Result<(User User, Order Order)> GetUserAndOrder(int userId, int orderId)
{
    var userResult = ResultPipeline<User>
        .Start(userId)
        .Then(id => _userRepository.GetUser(id))
        .Validate(user => user != null, ErrorCode.NotFound, $"User {userId} not found")
        .Build();

    var orderResult = ResultPipeline<Order>
        .Start(orderId)
        .Then(id => _orderRepository.GetOrder(id))
        .Validate(order => order != null, ErrorCode.NotFound, $"Order {orderId} not found")
        .Build();

    return Result.Combine(userResult, orderResult);
}

Migration Checklist

  1. Identify Error Handling Patterns
  2. Find try-catch blocks
  3. Locate null checks
  4. Identify custom result types

  5. Replace Exception Throwing

  6. Convert throw statements to Result.Failure
  7. Use appropriate error codes
  8. Add error metadata

  9. Update Method Signatures

  10. Change return types to Result
  11. Update async methods to return Task>
  12. Modify method documentation

  13. Refactor Validation

  14. Replace if statements with Validate
  15. Use batch validation where appropriate
  16. Implement custom validation rules

  17. Update Error Handling

  18. Replace catch blocks with Catch
  19. Use appropriate error recovery strategies
  20. Add error logging

  21. Test Migration

  22. Verify success cases
  23. Test error scenarios
  24. Check performance impact

  25. Document Changes

  26. Update API documentation
  27. Add migration notes
  28. Document breaking changes

Breaking Changes

  1. Method Signatures
  2. Return types changed to Result
  3. Async methods return Task>

  4. Error Handling

  5. Exceptions replaced with Result.Failure
  6. Custom error types replaced with Error

  7. Validation

  8. Validation logic moved to Validate method
  9. Custom validation rules need adaptation

  10. Async Operations

  11. ConfigureAwait usage required
  12. Parallel processing patterns changed

Tips for Smooth Migration

  1. Start Small
  2. Begin with simple methods
  3. Migrate one component at a time
  4. Test thoroughly after each change

  5. Use Automation

  6. Create migration scripts
  7. Use code analysis tools
  8. Automate testing

  9. Maintain Compatibility

  10. Keep old methods temporarily
  11. Use adapter patterns
  12. Phase out old code gradually

  13. Document Progress

  14. Track migrated components
  15. Note issues encountered
  16. Share lessons learned

  17. Get Feedback

  18. Involve team members
  19. Gather user feedback
  20. Adjust approach as needed