Extension Methods Reference
Complete reference for all extension methods provided by UnionGenerator to enhance union type usage.
π¦ Available Extensionsβ
UnionGenerator provides two sets of extension methods:
- MatchVoid Extensions - Simplified matching for void/Unit result types
- ResultComposition Extensions - Monadic operations (Bind, Map, MapError)
π§ MatchVoid Extensionsβ
Simplifies pattern matching when you want to perform side effects without returning a value.
Namespaceβ
using UnionGenerator.Extensions;
Why MatchVoid?β
Standard Match requires returning a value from all handlers. When performing side effects (logging, I/O, mutations), this is verbose:
// β Verbose - Need to return dummy values
result.Match(
ok: _ => { LogSuccess(); return Unit.Value; },
error: err => { LogError(err); return Unit.Value; }
);
// β
Concise - MatchVoid handles it
result.MatchVoid(
ok: () => LogSuccess(),
error: err => LogError(err)
);
MatchVoid (Required Handlers)β
Matches against all cases with action delegates. Both handlers are required.
Signatureβ
public static void MatchVoid<TError>(
this dynamic result,
Action ok,
Action<TError> error)
Parametersβ
| Parameter | Type | Description |
|---|---|---|
result | dynamic | The result to match. Must have IsSuccess/IsOk property and Error/ErrorValue property |
ok | Action | Action to execute if result is successful (no parameters) |
error | Action<TError> | Action to execute if result contains error, receiving the error value |
Returnsβ
void - Executes the appropriate action based on the result state
Exceptionsβ
ArgumentNullException- Ifresult,ok, orerroris nullInvalidOperationException- If result type doesn't have recognizable success/error properties
Example Usageβ
using UnionGenerator.Extensions;
[GenerateUnion]
public partial class Result<T, TError>
{
public static partial Result<T, TError> Ok(T value);
public static partial Result<T, TError> Error(TError error);
}
// Using MatchVoid
Result<Unit, ValidationError> validationResult = ValidateUser(user);
validationResult.MatchVoid(
ok: () => Console.WriteLine("Validation passed"),
error: err => Console.WriteLine($"Validation failed: {err.Message}")
);
Real-World Exampleβ
public async Task ProcessPayment(PaymentRequest request)
{
Result<Unit, PaymentError> result = await _paymentService.ProcessAsync(request);
result.MatchVoid(
ok: () =>
{
_logger.LogInformation("Payment processed successfully for request {RequestId}", request.Id);
_metrics.IncrementCounter("payments.success");
},
error: err =>
{
_logger.LogError("Payment failed: {Error}", err);
_metrics.IncrementCounter("payments.failed");
_notificationService.NotifyFailure(err);
}
);
}
Characteristicsβ
- Performance: O(1) - Single property check and action invocation
- Thread Safety: Thread-safe if actions are thread-safe
- Side Effects: Designed for side effects (logging, I/O, mutations)
- Allocations: No allocations beyond action closures
MatchVoid (Optional Handlers)β
Matches with optional handlers. Allows handling only the cases you care about.
Signatureβ
public static void MatchVoid<TError>(
this dynamic result,
Action? ok,
Action<TError>? error)
Parametersβ
| Parameter | Type | Description |
|---|---|---|
result | dynamic | The result to match |
ok | Action? | Optional action for success case. If null, success is ignored |
error | Action<TError>? | Optional action for error case. If null, error is ignored |
Returnsβ
void - Executes the appropriate action if provided
Example Usageβ
// Handle only errors
result.MatchVoid<ValidationError>(
ok: null,
error: err => Console.WriteLine($"Error: {err.Message}")
);
// Handle only success
result.MatchVoid<ValidationError>(
ok: () => Console.WriteLine("Success!"),
error: null
);
// No-op if neither provided
result.MatchVoid<ValidationError>(ok: null, error: null);
Use Casesβ
β Error Logging Only
public void LogErrors(Result<Unit, Error> result)
{
result.MatchVoid<Error>(
ok: null, // Don't care about success
error: err => _logger.LogError("Operation failed: {Error}", err)
);
}
β Success Notifications Only
public void NotifySuccess(Result<Unit, Error> result)
{
result.MatchVoid<Error>(
ok: () => _notifications.Send("Operation completed!"),
error: null // Don't care about errors
);
}
π ResultComposition Extensionsβ
Monadic operations for composing and transforming Result types. Enables functional-style chaining of operations.
Namespaceβ
using UnionGenerator.Extensions;
Why Monadic Composition?β
Allows chaining operations that can fail without nested error handling:
// β Without composition - nested and verbose
var userResult = GetUser(id);
if (userResult.IsOk)
{
var validationResult = ValidateUser(userResult.Value);
if (validationResult.IsOk)
{
var saveResult = SaveUser(validationResult.Value);
return saveResult;
}
return validationResult;
}
return userResult;
// β
With composition - flat and readable
return GetUser(id)
.Bind(user => ValidateUser(user))
.Bind(user => SaveUser(user));
Bindβ
Chains operations that return Results. Short-circuits on first error.
Signatureβ
public static dynamic Bind<TSuccess, TSuccess2, TError>(
this dynamic result,
Func<TSuccess, dynamic> binder)
Parametersβ
| Parameter | Type | Description |
|---|---|---|
result | dynamic | Source result to bind from |
binder | Func<TSuccess, dynamic> | Function that takes success value and returns a new Result |
Returnsβ
- If source is success: Result returned by
binder - If source is error: Source error (propagated, binder not called)
Exceptionsβ
ArgumentNullException- Ifresultorbinderis nullInvalidOperationException- If result type doesn't have recognizable properties
Example Usageβ
[GenerateUnion]
public partial class Result<T, TError>
{
public static partial Result<T, TError> Ok(T value);
public static partial Result<T, TError> Error(TError error);
}
// Chain multiple operations
Result<User, ValidationError> GetUser(int id) { /* ... */ }
Result<User, ValidationError> ValidateUser(User user) { /* ... */ }
Result<User, ValidationError> SaveUser(User user) { /* ... */ }
var result = GetUser(userId)
.Bind(user => ValidateUser(user))
.Bind(user => SaveUser(user));
Real-World Exampleβ
public Result<OrderConfirmation, OrderError> ProcessOrder(OrderRequest request)
{
return ValidateOrder(request)
.Bind(order => CheckInventory(order))
.Bind(order => ProcessPayment(order))
.Bind(order => CreateShipment(order))
.Bind(order => SendConfirmation(order));
// Stops at first error - remaining steps not executed
}
Monadic Lawsβ
Bind satisfies the monad laws:
Left Identity:
Result<T, E>.Ok(x).Bind(f) == f(x)
Right Identity:
result.Bind(x => Result<T, E>.Ok(x)) == result
Associativity:
result.Bind(f).Bind(g) == result.Bind(x => f(x).Bind(g))
Characteristicsβ
- Performance: O(1) for error case (returns immediately), O(n) for success (cost of binder)
- Short-Circuiting: Stops at first error, remaining operations not executed
- Thread Safety: Thread-safe if binder is thread-safe
- Allocations: No allocations except binder's return value
Mapβ
Transforms the success value without changing the Result structure.
Signatureβ
public static dynamic Map<TSuccess, TSuccess2, TError>(
this dynamic result,
Func<TSuccess, TSuccess2> mapper)
Parametersβ
| Parameter | Type | Description |
|---|---|---|
result | dynamic | Source result |
mapper | Func<TSuccess, TSuccess2> | Function to transform success value |
Returnsβ
- If source is success: New result with mapped success value
- If source is error: Source error unchanged
Example Usageβ
Result<int, string> GetNumber() => Result<int, string>.Ok(10);
Result<int, string> doubled = GetNumber()
.Map(x => x * 2);
Result<string, string> formatted = GetNumber()
.Map(x => $"Value: {x}");
// Chain multiple maps
Result<int, string> transformed = GetNumber()
.Map(x => x * 2)
.Map(x => x + 10)
.Map(x => x / 2);
Real-World Exampleβ
public Result<UserDto, DatabaseError> GetUserById(int id)
{
return _repository.FindUser(id)
.Map(user => new UserDto
{
Id = user.Id,
Name = user.FullName,
Email = user.EmailAddress
});
}
Characteristicsβ
- Pure Transformation: Does not perform I/O or side effects
- Performance: O(n) where n is cost of mapper function
- Error Preservation: Errors pass through unchanged
- Type Safety: Return type changes based on mapper
MapErrorβ
Transforms the error value without changing the success value.
Signatureβ
public static dynamic MapError<TSuccess, TError, TError2>(
this dynamic result,
Func<TError, TError2> mapper)
Parametersβ
| Parameter | Type | Description |
|---|---|---|
result | dynamic | Source result |
mapper | Func<TError, TError2> | Function to transform error value |
Returnsβ
- If source is success: Source success unchanged
- If source is error: New result with mapped error value
Example Usageβ
public Result<User, ApiError> GetUser(int id)
{
Result<User, DatabaseError> dbResult = _repository.FindUser(id);
// Convert database errors to API errors
return dbResult.MapError(dbErr => new ApiError
{
Code = "DATABASE_ERROR",
Message = dbErr.Message,
StatusCode = 500
});
}
Real-World Exampleβ
public async Task<Result<User, ProblemDetails>> GetUserForApi(int id)
{
Result<User, DomainError> domainResult = await _userService.GetUserAsync(id);
// Convert domain errors to HTTP problem details
return domainResult.MapError(err => err switch
{
NotFoundError nf => new ProblemDetails
{
Status = 404,
Title = "User Not Found",
Detail = nf.Message
},
ValidationError ve => new ProblemDetails
{
Status = 400,
Title = "Validation Failed",
Detail = ve.Message
},
_ => new ProblemDetails
{
Status = 500,
Title = "Internal Server Error",
Detail = "An unexpected error occurred"
}
});
}
Use Casesβ
β Layer Translation
// Domain β API
domainResult.MapError(err => err.ToApiError());
// Database β Domain
dbResult.MapError(err => err.ToDomainError());
β Error Enrichment
result.MapError(err => new EnrichedError
{
Original = err,
Timestamp = DateTime.UtcNow,
UserId = _currentUser.Id
});
β Localization
result.MapError(err => new LocalizedError
{
Code = err.Code,
Message = _localizer[err.MessageKey]
});
Characteristicsβ
- Success Preservation: Success values pass through unchanged
- Performance: O(n) where n is cost of mapper function
- Type Conversion: Allows changing error type in result chain
π Combining Extensionsβ
These extensions work together for powerful compositions:
Example: Full Pipelineβ
public async Task<Result<OrderConfirmation, ApiError>> ProcessOrderRequest(OrderRequest request)
{
return ValidateOrderRequest(request)
.Bind(req => CheckInventory(req))
.Bind(req => CalculatePricing(req))
.Map(priced => ApplyDiscounts(priced))
.Bind(order => ProcessPayment(order))
.Bind(paid => CreateShipment(paid))
.Map(shipped => new OrderConfirmation(shipped))
.MapError(err => ConvertToApiError(err))
.MatchVoid<ApiError>(
ok: () => _logger.LogInformation("Order processed successfully"),
error: err => _logger.LogError("Order processing failed: {Error}", err)
);
}
Example: Validation Pipelineβ
public Result<User, ValidationError> ValidateAndCreate(UserCreateRequest request)
{
return ValidateEmail(request.Email)
.Bind(_ => ValidatePassword(request.Password))
.Bind(_ => ValidateUsername(request.Username))
.Bind(_ => CheckUsernameTaken(request.Username))
.Map(_ => new User
{
Email = request.Email,
Username = request.Username,
PasswordHash = HashPassword(request.Password)
});
}
β‘ Performance Considerationsβ
| Operation | Allocations | Complexity | Notes |
|---|---|---|---|
MatchVoid | Closure only | O(1) | Very lightweight |
Bind | Result object | O(1) + binder cost | Short-circuits on error |
Map | Result object | O(1) + mapper cost | Pure transformation |
MapError | Result object | O(1) + mapper cost | Errors only |
Performance Tipsβ
- Prefer Bind chains - More efficient than nested pattern matching
- Avoid allocations in mappers - Keep mapper functions lightweight
- Short-circuit early - Put cheap validations first
- Use MatchVoid for side effects - Don't return dummy values
π Thread Safetyβ
All extension methods are thread-safe with these guarantees:
- β Methods don't modify shared state
- β Safe to call from multiple threads
- β οΈ Thread safety of delegates is caller's responsibility
π§© Integration with LINQβ
These extensions enable LINQ query syntax:
// Query expression syntax (if SelectMany is implemented)
var result = from user in GetUser(id)
from validated in ValidateUser(user)
from saved in SaveUser(validated)
select saved;
// Equivalent to:
var result = GetUser(id)
.Bind(user => ValidateUser(user))
.Bind(user => SaveUser(user));
π Next Stepsβ
- Review Pattern Matching for more matching patterns
- See Common Patterns for real-world examples
- Check Best Practices for production guidance
π Additional Resourcesβ
- Generated API Reference - Complete API documentation
- Attributes Reference - Configuration options
- Result Pattern Guide - Result type patterns