Migration Guide
This guide helps you migrate from traditional exception-based error handling or other Result libraries to Pragmatic.Result.
From Exception-Based Code
Section titled “From Exception-Based Code”Before (Exceptions)
Section titled “Before (Exceptions)”public User GetUser(int id){ var user = _db.Users.Find(id); if (user == null) throw new NotFoundException($"User {id} not found"); return user;}
// Caller must use try-catchtry{ var user = GetUser(123); Console.WriteLine(user.Name);}catch (NotFoundException ex){ Console.WriteLine("User not found");}After (Result)
Section titled “After (Result)”public Result<User, NotFoundError> GetUser(int id){ var user = _db.Users.Find(id); if (user == null) return NotFoundError.Create("User", id); return user;}
// Caller uses Match or pattern matchingvar user = GetUser(123);user.Match( u => Console.WriteLine(u.Name), e => Console.WriteLine("User not found"));
// Or using TryGetValueif (user.TryGetValue(out var u)) Console.WriteLine(u.Name);Key Migration Patterns
Section titled “Key Migration Patterns”1. Return Type Changes
Section titled “1. Return Type Changes”| Before | After |
|---|---|
User GetUser() | Result<User, NotFoundError> GetUser() |
void DeleteUser() | VoidResult<NotFoundError> DeleteUser() |
User? GetUserOrNull() | Maybe<User> GetUser() |
2. Exception to Error Type Mapping
Section titled “2. Exception to Error Type Mapping”| Exception | Error Type |
|---|---|
NotFoundException | NotFoundError |
UnauthorizedAccessException | UnauthorizedError |
InvalidOperationException | ForbiddenError or BadRequestError |
DuplicateKeyException | ConflictError |
ArgumentException | BadRequestError |
3. Repository Pattern
Section titled “3. Repository Pattern”Before
Section titled “Before”public interface IUserRepository{ User GetById(int id); // throws NotFoundException User? FindById(int id); // returns null void Add(User user); // throws ConflictException}public interface IUserRepository{ Result<User, NotFoundError> GetById(int id); Maybe<User> FindById(int id); VoidResult<ConflictError> Add(User user);}4. Service Layer
Section titled “4. Service Layer”Before
Section titled “Before”public class OrderService{ public Order PlaceOrder(OrderRequest request) { if (!ValidateRequest(request)) throw new ValidationException("Invalid request");
var user = _userRepo.GetById(request.UserId); if (!user.CanPlaceOrders) throw new ForbiddenException("User cannot place orders");
var order = new Order(user, request.Items); _orderRepo.Add(order); return order; }}public class OrderService{ public Result<Order, PlaceOrderError> PlaceOrder(OrderRequest request) { return ValidateRequest(request) .Bind(_ => _userRepo.GetById(request.UserId) .MapError(e => PlaceOrderError.UserNotFound(e))) .Ensure( user => user.CanPlaceOrders, _ => PlaceOrderError.UserCannotOrder()) .Map(user => new Order(user, request.Items)) .Tap(order => _orderRepo.Add(order)); }}5. Controller/API Layer
Section titled “5. Controller/API Layer”Before
Section titled “Before”[HttpGet("{id}")]public ActionResult<User> GetUser(int id){ try { var user = _userService.GetUser(id); return Ok(user); } catch (NotFoundException) { return NotFound(); } catch (UnauthorizedException) { return Unauthorized(); }}After (Automatic Handling)
Section titled “After (Automatic Handling)”// Register filter in Program.csbuilder.Services.AddControllers(options =>{ options.Filters.Add<ResultActionFilter>();});
// Controller returns Result directly[HttpGet("{id}")]public async Task<Result<User, NotFoundError>> GetUser(int id) => await _userService.GetUserAsync(id);// Automatic: 200 OK with user, or 404 ProblemDetailsFrom Other Result Libraries
Section titled “From Other Result Libraries”From ErrorOr
Section titled “From ErrorOr”| ErrorOr | Pragmatic.Result |
|---|---|
ErrorOr<T> | Result<T, IError> or Result<T, E1, E2, ...> |
.ThenDo(action) | .Tap(action) or .OnSuccess(action) |
.FailIf(pred, error) | .Ensure(pred, errorFactory) |
.Value | .Value |
.Errors | .Error (single) or pattern match |
// ErrorOrErrorOr<User> GetUser() => ...;var result = GetUser() .ThenDo(u => Log(u)) .FailIf(u => !u.IsActive, Error.Validation("Inactive"));
// Pragmatic.ResultResult<User, NotFoundError> GetUser() => ...;var result = GetUser() .Tap(u => Log(u)) .Ensure(u => u.IsActive, _ => new InactiveUserError());From FluentResults
Section titled “From FluentResults”| FluentResults | Pragmatic.Result |
|---|---|
Result<T> | Result<T, TError> |
.IsSuccess | .IsSuccess |
.Value | .Value |
.Errors | .Error |
.OnSuccess(action) | .OnSuccess(action) |
.OnFailure(action) | .OnFailure(action) |
.Bind(func) | .Bind(func) |
Key difference: Pragmatic.Result uses typed errors, not List<IError>.
From OneOf
Section titled “From OneOf”| OneOf | Pragmatic.Result |
|---|---|
OneOf<T, Error> | Result<T, Error> |
.Match(...) | .Match(...) |
.AsT0 | .Value |
.IsT0 | .IsSuccess |
// OneOfOneOf<User, NotFound, Forbidden> GetUser();result.Match( user => Ok(user), notFound => NotFound(), forbidden => Forbid());
// Pragmatic.Result (with multi-error)Result<User, NotFoundError, ForbiddenError> GetUser();result.Match( user => Ok(user), notFound => NotFound(), forbidden => Forbid());Bridging with Existing Code
Section titled “Bridging with Existing Code”TryCatch for Legacy Integration
Section titled “TryCatch for Legacy Integration”Use TryCatch to bridge existing exception-throwing code:
// Wrap legacy codepublic Result<Data, BadRequestError> GetLegacyData(string id){ return ResultExtensions.TryCatch( () => _legacyService.GetData(id), ex => BadRequestError.Create(ex.Message));}
// Async versionpublic Task<Result<Data, BadRequestError>> GetLegacyDataAsync(string id){ return ResultExtensions.TryCatchAsync( () => _legacyService.GetDataAsync(id), ex => BadRequestError.Create(ex.Message));}Gradual Migration Strategy
Section titled “Gradual Migration Strategy”- Start at boundaries: Convert API endpoints first
- Work inward: Migrate services, then repositories
- Use TryCatch: Bridge legacy code during transition
- Update tests: Convert exception assertions to Result assertions
When NOT to Use Result
Section titled “When NOT to Use Result”Keep using exceptions for:
- Programming errors (bugs):
ArgumentNullException,IndexOutOfRangeException - Infrastructure failures: Database connection lost, file system errors
- Unrecoverable states: Out of memory, stack overflow
Use Result for:
- Business rule violations: User not found, insufficient permissions
- Validation failures: Invalid input, constraint violations
- Expected failures: Cache miss, optional data not available
Testing Migration
Section titled “Testing Migration”Before
Section titled “Before”[Fact]public void GetUser_NotFound_ThrowsException(){ var service = new UserService();
Assert.Throws<NotFoundException>(() => service.GetUser(999));}[Fact]public void GetUser_NotFound_ReturnsFailure(){ var service = new UserService();
var result = service.GetUser(999);
result.IsFailure.Should().BeTrue(); result.Error.Code.Should().Be("NOT_FOUND");}