Integration Extensions API Reference
Integration extensions provide seamless interoperability between Result and OneOf patterns, enabling mixed workflows, gradual migration, and flexible architecture designs.
ToOneOf(this Result result, Func errorMapper)
Converts a Result to OneOf using custom error mapping.
Result<User> result = GetUserFromDatabase(id);
OneOf<ValidationError, User> oneOf = result.ToOneOf(reason => new ValidationError(reason.Message));
Parameters:
- result (ResulterrorMapper (Func
Returns: OneOf<TError, T> containing TError on failure or T on success
Throws: ArgumentNullException if errorMapper is null
ToResult(this OneOf oneOf, Func errorMapper)
Converts a OneOf to Result using custom error mapping.
OneOf<ApiError, User> oneOf = GetUserFromApi(id);
Result<User> result = oneOf.ToResult(error => new Error(error.Message));
Parameters:
- oneOf (OneOferrorMapper (Func
Returns: Result<T> containing T on success or IError on failure
Throws: ArgumentNullException if errorMapper is null
SelectToResult(this OneOf oneOf, Func selector, Func? errorMapper = null)
Transforms the success value of a OneOf using a selector function, returning a Result.
OneOf<string, User> oneOf = GetUser(id);
Result<UserDto> result = oneOf.SelectToResult(
user => new UserDto(user.Name),
error => new Error($"User error: {error}")
);
Parameters:
- oneOf (OneOfselector (FuncerrorMapper (Func
Returns: Result<TResult> containing transformed success or mapped error
Throws: ArgumentNullException if selector is null
SelectToResult(this OneOf oneOf, Func selector) where T1 : IError
Transforms the success value when T1 is an IError type, using the error directly.
OneOf<ApiError, User> oneOf = GetUserFromApi(id);
Result<UserDto> result = oneOf.SelectToResult(user => new UserDto(user.Name));
Parameters:
- oneOf (OneOfselector (Func
Returns: Result<TResult> containing transformed success or original IError
Throws: ArgumentNullException if selector is null
BindToResult(this OneOf oneOf, Func> binder, Func? errorMapper = null)
Binds the success value of a OneOf to a Result-producing function.
OneOf<string, User> oneOf = GetUser(id);
Result<User> result = oneOf.BindToResult(
user => ValidateUser(user),
error => new Error($"Bind error: {error}")
);
Parameters:
- oneOf (OneOfbinder (FuncerrorMapper (Func
Returns: Result<TResult> from binder or mapped error
Throws: ArgumentNullException if binder is null
BindToResult(this OneOf oneOf, Func> binder) where T1 : IError
Binds the success value when T1 is an IError type, using the error directly.
OneOf<ApiError, User> oneOf = GetUserFromApi(id);
Result<User> result = oneOf.BindToResult(user => ValidateUser(user));
Parameters:
- oneOf (OneOfbinder (Func
Returns: Result<TResult> from binder or original IError
Throws: ArgumentNullException if binder is null
Filter(this OneOf oneOf, Func predicate)
Filters a OneOf based on a predicate, returning the original OneOf if the predicate passes.
OneOf<Error, User> oneOf = GetUser(id);
OneOf<Error, User> active = oneOf.Filter(user => user.IsActive);
Parameters:
- oneOf (OneOfpredicate (Func
Returns: OneOf<T1, T2> if predicate passes, throws InvalidOperationException otherwise
Throws: ArgumentNullException if predicate is null
Throws: InvalidOperationException if predicate returns false
ToOneOfCustom(this Result result, Func errorMapper)
Converts a Result to OneOf using custom error mapping (alternative to basic ToOneOf).
Result<User> result = GetUserFromDatabase(id);
OneOf<ValidationError, User> oneOf = result.ToOneOfCustom(reason => new ValidationError(reason.Message));
Parameters:
- result (ResulterrorMapper (Func
Returns: OneOf<T1, T2> containing T1 on failure or T2 on success
Throws: ArgumentNullException if errorMapper is null
public class UserService
{
// API layer returns OneOf
public OneOf<ApiError, User> GetUserFromApi(int id)
{
try
{
var response = _httpClient.GetAsync($"/api/users/{id}").Result;
return response.IsSuccessStatusCode
? JsonSerializer.Deserialize<User>(response.Content.ReadAsStringAsync().Result)
: new ApiError("User not found", 404);
}
catch (Exception ex)
{
return new ApiError(ex.Message, 500);
}
}
// Business layer uses Result
public Result<UserDto> GetUserDto(int id)
{
return GetUserFromApi(id)
.SelectToResult(user => new UserDto(user.Name, user.Email));
}
// Database layer expects OneOf
public OneOf<ValidationError, UserDto> GetUserForDatabase(int id)
{
return GetUserDto(id)
.ToOneOfCustom(reason => new ValidationError(reason.Message));
}
}
public class OrderProcessor
{
public OneOf<BusinessError, ProcessedOrder> ProcessOrder(OrderRequest request)
{
// Step 1: Validate request (returns OneOf)
OneOf<ValidationError, ValidatedRequest> validated = ValidateRequest(request);
// Step 2: Convert to Result for business logic
Result<ValidatedRequest> businessResult = validated.SelectToResult(req => req);
// Step 3: Process with Result workflow
Result<Order> orderResult = businessResult.BindToResult(req => CreateOrder(req));
// Step 4: Convert back to OneOf for response
return orderResult.ToOneOfCustom(reason => new BusinessError(reason.Message))
.BindToResult(order => ProcessOrder(order));
}
private OneOf<ValidationError, ValidatedRequest> ValidateRequest(OrderRequest request)
{
return request.IsValid()
? OneOf<ValidationError, ValidatedRequest>.FromT2(new ValidatedRequest(request))
: OneOf<ValidationError, ValidatedRequest>.FromT1(new ValidationError("Invalid request"));
}
private Result<Order> CreateOrder(ValidatedRequest request)
{
try
{
var order = new Order(request);
_database.Save(order);
return Result<Order>.Ok(order);
}
catch (Exception ex)
{
return Result<Order>.Fail($"Failed to create order: {ex.Message}");
}
}
private OneOf<BusinessError, ProcessedOrder> ProcessOrder(Order order)
{
try
{
var processed = _orderProcessor.Process(order);
return OneOf<BusinessError, ProcessedOrder>.FromT2(processed);
}
catch (Exception ex)
{
return OneOf<BusinessError, ProcessedOrder>.FromT1(new BusinessError(ex.Message));
}
}
}
public class ErrorTransformer
{
// Transform between different error types
public OneOf<DomainError, User> TransformApiUser(OneOf<ApiError, User> apiUser)
{
return apiUser.SelectToResult(
user => user,
apiError => new Error($"API Error: {apiError.Message}")
).ToOneOf(error => new DomainError(error.Message));
}
// Filter and transform in pipeline
public OneOf<ValidationError, ActiveUser> GetActiveUser(int id)
{
return GetUser(id)
.Filter(user => user.IsActive)
.BindToResult(user => ValidateActiveUser(user))
.ToOneOf(reason => new ValidationError(reason.Message));
}
private OneOf<DatabaseError, User> GetUser(int id)
{
var user = _database.FindUser(id);
return user != null
? OneOf<DatabaseError, User>.FromT2(user)
: OneOf<DatabaseError, User>.FromT1(new DatabaseError("User not found"));
}
private Result<ActiveUser> ValidateActiveUser(User user)
{
return user.IsValid()
? Result<ActiveUser>.Ok(new ActiveUser(user))
: Result<ActiveUser>.Fail("User is not valid for activation");
}
}
public class MigrationExample
{
// Legacy code using Result
public Result<User> GetUserLegacy(int id)
{
try
{
var user = _database.FindUser(id);
return user != null
? Result<User>.Ok(user)
: Result<User>.Fail("User not found");
}
catch (Exception ex)
{
return Result<User>.Fail(ex.Message);
}
}
// New code using OneOf
public OneOf<NotFoundError, User> GetUserNew(int id)
{
try
{
var user = _database.FindUser(id);
return user != null
? OneOf<NotFoundError, User>.FromT2(user)
: OneOf<NotFoundError, User>.FromT1(new NotFoundError(id));
}
catch (Exception ex)
{
return OneOf<NotFoundError, User>.FromT1(new NotFoundError(id, ex.Message));
}
}
// Bridge between old and new
public OneOf<NotFoundError, User> GetUserWithMigration(int id, bool useNewImplementation)
{
if (useNewImplementation)
{
return GetUserNew(id);
}
else
{
// Convert legacy Result to new OneOf
return GetUserLegacy(id).ToOneOf(reason => new NotFoundError(id, reason.Message));
}
}
// Gradual migration - can work with both patterns
public string ProcessUser(int id)
{
var legacyResult = GetUserLegacy(id);
var newResult = GetUserNew(id);
// Can process both types
string legacyMessage = legacyResult.Match(
error => $"Legacy error: {error.Message}",
user => $"Legacy user: {user.Name}"
);
string newMessage = newResult.Match(
error => $"New error: {error.Message}",
user => $"New user: {user.Name}"
);
return $"{legacyMessage} | {newMessage}";
}
}
public class ConfigurationManager
{
// Parse configuration with multiple result types
public OneOf<ConfigError, int> GetTimeoutSetting()
{
var timeoutString = _configuration["timeout"];
return int.TryParse(timeoutString, out var timeout)
? timeout > 0
? OneOf<ConfigError, int>.FromT2(timeout)
: OneOf<ConfigError, int>.FromT1(new ConfigError("Timeout must be positive"))
: OneOf<ConfigError, int>.FromT1(new ConfigError("Invalid timeout format"));
}
// Use in Result workflow
public Result<ValidatedConfig> ValidateConfiguration()
{
return GetTimeoutSetting()
.SelectToResult(timeout => new ValidatedConfig(timeout))
.BindToResult(config => ValidateConfig(config));
}
// Convert to OneOf for API response
public OneOf<ValidationError, ValidatedConfig> GetConfigForApi()
{
return ValidateConfiguration()
.ToOneOfCustom(reason => new ValidationError(reason.Message));
}
private Result<ValidatedConfig> ValidateConfig(ValidatedConfig config)
{
return config.Timeout <= 300
? Result<ValidatedConfig>.Ok(config)
: Result<ValidatedConfig>.Fail("Timeout cannot exceed 300 seconds");
}
}
| Scenario | Recommended Method | Reason |
|---|---|---|
| Simple error mapping | ToResult/ToOneOf with IError constraint |
Direct usage, no mapping overhead |
| Complex error transformation | SelectToResult/BindToResult with custom mapper |
Full control over error mapping |
| Filtering operations | Filter |
Built-in predicate support |
| Migration scenarios | ToOneOfCustom |
Alternative mapping approach |
Memory Allocation
- Direct IError methods: Zero additional allocation for error mapping
- Custom mapper methods: One allocation for mapped error object
- Filter method: No allocation on success, throws on failure
Performance Tips
- Use IError overloads when T1 implements IError for best performance
- Avoid complex mappers in hot paths - consider pre-mapped error types
- Use Filter for simple predicates instead of Bind with conditional logic
- Cache mapper functions if they're complex and reused frequently
✅ Do Use
- IError overloads for common error/success patterns
- Custom mappers for error type transformations
- Filter for conditional processing
- Mixed pipelines when different layers use different patterns
❌ Avoid
- Complex mappers that throw exceptions
- Nested conversions without clear purpose
- Magic strings in error mapping
- Forgetting null checks in custom mappers
From Result to OneOf
// Before: Result pattern
Result<User> result = GetUser(id);
if (result.IsSuccess)
{
return ProcessUser(result.Value);
}
else
{
return HandleError(result.Errors.First());
}
// After: OneOf pattern
OneOf<Error, User> oneOf = result.ToOneOf(error => new Error(error.Message));
return oneOf.Match(
error => HandleError(error),
user => ProcessUser(user)
);
From OneOf to Result
// Before: OneOf pattern
OneOf<Error, User> oneOf = GetUser(id);
return oneOf.Match(
error => Result<User>.Fail(error.Message),
user => Result<User>.Ok(user)
);
// After: Result pattern
Result<User> result = oneOf.ToResult(error => new Error(error.Message));
return result;
Gradual Migration
// Layer 1: API (OneOf)
public OneOf<ApiError, User> GetApiUser(int id) { /* ... */ }
// Layer 2: Business (Result) - can work with both
public Result<UserDto> GetUserDto(int id)
{
return GetApiUser(id).SelectToResult(user => user.ToDto());
}
// Layer 3: Database (OneOf) - can work with both
public OneOf<DbError, UserDto> SaveUser(UserDto dto)
{
return ValidateUser(dto)
.ToOneOfCustom(reason => new DbError(reason.Message))
.BindToResult(user => _database.Save(user))
.ToOneOf(error => new DbError(error.Message));
}
- Advanced Patterns Guide - Usage patterns and integration scenarios
- Maybe API - Optional value patterns
- OneOf API - Discriminated union patterns
- OneOf3 API - Three-way discriminated unions