-
Notifications
You must be signed in to change notification settings - Fork 0
Result Type Extension Methods
The ZeidLab.ToolBox.Result
namespace consolidates various extension methods that streamline conversion and error handling across monadic types. By leveraging these extension methods, developers can create robust, clear, and efficient code that gracefully handles both successful outcomes and error states, ultimately improving both maintainability and
performance.
-
Unified Conversions:
Methods like
ToSuccess
,ToFailure
, andToMaybe
enable implicit conversions between plain values, errors, and their monadic wrappers. This approach encourages the railway-oriented programming style by allowing errors to be managed as first-class citizens, thereby reducing the reliance on exceptions. -
Asynchronous Support:
Overloads such as
ToMaybeAsync
andToUnitResultAsync
extend the same conversion principles to asynchronous operations, ensuring that asynchronous workflows remain as expressive and error-resilient as their synchronous counterparts. -
Performance-Oriented Design:
Each method is decorated with
[MethodImpl(MethodImplOptions.AggressiveInlining)]
, which hints to the compiler for inlining, reducing method call overhead and potentially enhancing performance—particularly beneficial in high-throughput or performance-critical applications. -
Consolidated Overloads:
For methods with multiple overloads (e.g.,
ToFailure
andToMaybeAsync
), the documentation focuses on the overall conversion intent rather than the nuances of each overload. This provides a clearer, more unified developer experience.
Below are examples and descriptions of how to use the extension methods provided in the ZeidLab.ToolBox.Result
namespace. Any of the methods below are applicable to Result<TValue>
,Task<Result<TValue>>
and Try<TValue>
, and
TryAsync<TValue>
.Most of the methods resolve into Result<TValue>
, Task<Result<TValue>>
or TValue
depending on
the method.
Bind<TIn, TOut>
and BindAsync<TIn, TOut>
are essential for composing operations in railway-oriented programming by
chaining functions that yield Result values, thereby streamlining error handling. In essence, Bind<TIn, TOut>
takes a
successful Result (or Try) and applies a function to its value, passing the output to the next operation while
immediately propagating any error encountered—effectively acting as a "track switch" in the railway metaphor. Meanwhile,
BindAsync<TIn, TOut>
performs the same role for asynchronous workflows by handling Task<Result<T>>
types,
integrating seamlessly with async/await
patterns. This approach eliminates deeply nested conditional logic,
ensuring that operations only proceed when previous computations succeed and that any error interrupts the chain
in a predictable, and manageable way.
The below table summarizes the types and parameters of each method overload and their respective applicability.
Method | Applicable Type | Accepted Parameter | Returns |
---|---|---|---|
Bind<TIn, TOut> |
Result<TIn> |
Func<TIn, Result<TOut>> |
Result<TOut> |
Bind<TIn, TOut> |
Result<TIn> |
Func<TIn, Try<TOut>> |
Result<TOut> |
BindAsync<TIn, TOut> |
Result<TIn> |
Func<TIn, Task<Result<TOut>>> |
Task<Result<TOut>> |
BindAsync<TIn, TOut> |
Result<TIn> |
Func<TIn, TryAsync<TOut>> |
Task<Result<TOut>> |
Bind<TIn, TOut> |
Try<TIn> |
Func<TIn, Result<TOut>> |
Result<TOut> |
Bind<TIn, TOut> |
Try<TIn> |
Func<TIn, Try<TOut>> |
Result<TOut> |
BindAsync<TIn, TOut> |
Try<TIn> |
Func<TIn, Task<Result<TOut>>> |
Task<Result<TOut>> |
BindAsync<TIn, TOut> |
Try<TIn> |
Func<TIn, TryAsync<TOut>> |
Task<Result<TOut>> |
BindAsync<TIn, TOut> |
Task<Result<TIn>> |
Func<TIn, Result<TOut>> |
Task<Result<TOut>> |
BindAsync<TIn, TOut> |
Task<Result<TIn>> |
Func<TIn, Try<TOut>> |
Task<Result<TOut>> |
BindAsync<TIn, TOut> |
Task<Result<TIn>> |
Func<TIn, Task<Result<TOut>>> |
Task<Result<TOut>> |
BindAsync<TIn, TOut> |
Task<Result<TIn>> |
Func<TIn, TryAsync<TOut>> |
Task<Result<TOut>> |
BindAsync<TIn, TOut> |
TryAsync<TIn> |
Func<TIn, Result<TOut>> |
Task<Result<TOut>> |
BindAsync<TIn, TOut> |
TryAsync<TIn> |
Func<TIn, Try<TOut>> |
Task<Result<TOut>> |
BindAsync<TIn, TOut> |
TryAsync<TIn> |
Func<TIn, Task<Result<TOut>>> |
Task<Result<TOut>> |
BindAsync<TIn, TOut> |
TryAsync<TIn> |
Func<TIn, TryAsync<TOut>> |
Task<Result<TOut>> |
As you see in above table you can practically bind anything with anything, Below are few examples of how to use
Bind<TIn, TOut>
and BindAsync<TIn, TOut>
Example 1: Synchronous chain using Result<TIn>
// In this example, we start with a successful result, validate the number,
// and then double it. If any step fails, the error is immediately propagated.
Result<int> ValidatePositive(int number) =>
number > 0
? Result.Success(number)
: Result.Failure<int>(ResultError.New("Number must be positive"));
Result<int> DoubleValue(int number) =>
Result.Success(number * 2);
Result.Success(5)
.Bind(ValidatePositive) // Validates that 5 is positive.
.Bind(DoubleValue) // Doubles the number if valid.
.Match(
success: value => Console.WriteLine($"Success: {value}") , // executed
failure: error => Console.WriteLine($"Failure: {error.Message}") // skipped
); // Expected output: "Success: 10"
Example 2: Synchronous chain using Try<TIn>
// This example demonstrates parsing a string into an integer and then validating
// that the parsed number is even. The int.Parse is exception prone,
// so we wrapped it with Try<TIn> delegate to encapsulate
// potential exceptions and errors.
Try<int> ParseNumber(string input)
// implicit conversion and exception handling
=> () => int.Parse(input);
Try<int> ValidateEven(int number)
=> () => number % 2 == 0
// implicit conversion from both int to Result<int>.IsSuccess
// or Result<int>.IsFailure from ResultError
? number : ResultError.New("Number is not even");
ParseNumber("8") // returns Try<int>
.Bind(ValidateEven) // Chains Try operations: parsing then validating evenness.
.Match(
success: value => Console.WriteLine($"Parsed and validated: {value}"), // executed
failure: error => Console.WriteLine($"Error: {error.Message}") // skipped
); // Expected output: "Parsed and validated: 8"
ParseNumber("InvalidString") // returns Try<int>
.Bind(ValidateEven) // Returns Result.Failure<int> with Invalid Format Exception from previous operation
.Match(
success: value => Console.WriteLine($"Parsed and validated: {value}"), // skipped
failure: error => Console.WriteLine($"Error: {error.Message}") // executed
); // Expected output: "Invalid Format Exception Message"
ParseNumber("7") // returns Try<int>
.Bind(ValidateEven) // Returns Result.Failure<int> with "Number is not even" error
.Match(
success: value => Console.WriteLine($"Parsed and validated: {value}"), // skipped
failure: error => Console.WriteLine($"Error: {error.Message}") // executed
); // Expected output: "Number is not even"
Example 3: Asynchronous chain using BindAsync
// This example shows how to combine asynchronous and synchronous operations.
// We first fetch user data asynchronously and then validate it synchronously.
// The chain seamlessly integrates async operations using BindAsync.
async TryAsync<string> FetchUserDataAsync(int userId)
{
// Simulate asynchronous data fetching.
await Task.Delay(100);
return userId > 0
? Result.Success("UserData")
// implicit conversion from ResultError to Result<string>.IsFailure
: ResultError.New("Invalid user ID");
}
Result<string> ValidateUserData(string data)
{
// Synchronously validate the fetched data.
return data.Contains("User")
? Result.Success(data)
// implicit conversion from ResultError to Result<string>.IsFailure
: ResultError.New("Data validation failed");
}
var asyncResult = await Result.Success(1)
.BindAsync(FetchUserDataAsync) // Asynchronously fetch data.
.BindAsync(ValidateUserData); // Synchronously validate the data.
Console.WriteLine(asyncResult.IsSuccess
? $"Async Success: {asyncResult.Value}"
: $"Async Error: {asyncResult.Error.Message}");
// Expected output: "Async Success: UserData"
In railway-oriented programming, the Match<TIn>
and MatchAsync<TIn>
extension methods enable explicit handling of success and failure outcomes for Result<TIn>
types. Match<TIn>
synchronously evaluates a result, executing specified actions for success or failure, while MatchAsync<TIn>
performs the same operations asynchronously. This approach ensures clear and maintainable code by defining separate paths for each outcome. You need to pay attention that the Match<TIn>
and MatchAsync<TIn>
are designed to be the last member of the Result<TIn>
chain. There are other methods you can use in the middle of the chain, for example Bind<TIn, TOut>
and BindAsync<TIn, TOut>
.
The below table summarizes the types and parameters of each method overload and their respective applicability.
Method | Applicable Type | success parameter | failure parameter | Returns |
---|---|---|---|---|
Match<TIn, TOut> |
Result<TIn> |
Func<TIn, TOut> |
Func<ResultError, TOut> |
TOut |
MatchAsync<TIn, TOut> |
Result<TIn> |
Func<TIn, Task<TOut>> |
Func<ResultError, Task<TOut>> |
Task<TOut> |
Match<TIn> |
Result<TIn> |
Action<TIn> |
Action<ResultError> |
void |
MatchAsync<TIn> |
Result<TIn> |
Func<TIn, Task> |
Func<ResultError, Task> |
Task |
Match<TIn, TOut> |
Try<TIn> |
Func<TIn, TOut> |
Func<ResultError, TOut> |
TOut |
MatchAsync<TIn, TOut> |
Try<TIn> |
Func<TIn, Task<TOut>> |
Func<ResultError, Task<TOut>> |
Task<TOut> |
Match<TIn> |
Try<TIn> |
Action<TIn> |
Action<ResultError> |
void |
MatchAsync<TIn> |
Try<TIn> |
Func<TIn, Task> |
Func<ResultError, Task> |
Task |
MatchAsync<TIn, TOut> |
Task<Result<TIn>> |
Func<TIn, TOut> |
Func<ResultError, TOut> |
Task<TOut> |
MatchAsync<TIn, TOut> |
Task<Result<TIn>> |
Func<TIn, Task<TOut>> |
Func<ResultError, Task<TOut>> |
Task<TOut> |
MatchAsync<TIn> |
Task<Result<TIn>> |
Action<TIn> |
Action<ResultError> |
Task |
MatchAsync<TIn> |
Task<Result<TIn>> |
Func<TIn, Task> |
Func<ResultError, Task> |
Task |
MatchAsync<TIn, TOut> |
TryAsync<TIn> |
Func<TIn, TOut> |
Func<ResultError, TOut> |
Task<TOut> |
MatchAsync<TIn, TOut> |
TryAsync<TIn> |
Func<TIn, Task<TOut>> |
Func<ResultError, Task<TOut>> |
Task<TOut> |
MatchAsync<TIn> |
TryAsync<TIn> |
Action<TIn> |
Action<ResultError> |
Task |
MatchAsync<TIn> |
TryAsync<TIn> |
Func<TIn, Task> |
Func<ResultError, Task> |
Task |
As you see in above table all the types in this namespace Result<TIn>
, Try<TIn>
, Task<Result<TIn>>
and TryAsync<TIn>
are supported by Match<TIn>
and MatchAsync<TIn>
extension methods. Below are few examples of how to use
Match<TIn>
and MatchAsync<TIn>
Ensure<TIn>
and EnsureAsync<TIn>
are extension methods that act as validation checkpoints within a railway-oriented programming pipeline. They work by inspecting a successful Result<TIn>
—if the result is already a failure, it passes through unchanged; but if it’s a success, they evaluate a predicate on its value. If the predicate returns false, the method converts the successful result into a failure by returning a new error (provided by the caller); if the predicate passes, the original result is returned. The synchronous Ensure
handles immediate validations, while EnsureAsync
extends this behavior to asynchronous scenarios by awaiting the predicate's result, enabling seamless integration of both sync and async validations in a fluent, composable error-handling workflow.
The below table summarizes the types and parameters of each method overload and their respective applicability.
Method | Applicable Types | Accepted Predicate | Returns |
---|---|---|---|
Ensure<TIn> |
Result<TIn> |
Func<TIn, bool> |
Result<TIn> |
EnsureAsync<TIn> |
Result<TIn> |
Func<TIn, Task<bool>> |
Task<Result<TIn>> |
Ensure<TIn> |
Try<TIn> |
Func<TIn, bool> |
Result<TIn> |
EnsureAsync<TIn> |
Try<TIn> |
Func<TIn, Task<bool>> |
Task<Result<TIn>> |
EnsureAsync<TIn> |
Task<Result<TIn>> |
Func<TIn, bool> |
Task<Result<TIn>> |
EnsureAsync<TIn> |
Task<Result<TIn>> |
Func<TIn, Task<bool>> |
Task<Result<TIn>> |
EnsureAsync<TIn> |
TryAsync<TIn> |
Func<TIn, bool> |
Task<Result<TIn>> |
EnsureAsync<TIn> |
TryAsync<TIn> |
Func<TIn, Task<bool>> |
Task<Result<TIn>> |
As you see in above table all the types in this namespace Result<TIn>
, Try<TIn>
, Task<Result<TIn>>
and TryAsync<TIn>
are supported by Ensure<TIn>
and EnsureAsync<TIn>
extension methods. Below are few examples of how to use
Ensure<TIn>
and EnsureAsync<TIn>
Example: Synchronous Username Validation
This example validates a username by ensuring it isn’t empty and meets a minimum length requirement.
// Validate that the username is not empty and has at least 3 characters.
Result.Success("Alice")
.Ensure(name => !string.IsNullOrWhiteSpace(name), ResultError.New("Username cannot be empty"))
.Ensure(name => name.Length >= 3, ResultError.New("Username must have at least 3 characters"))
.Match(
success: name => Console.WriteLine( $"Username '{name}' is valid."),
failure: error => Console.WriteLine($"Validation error: {error.Message}")
);
Example: Asynchronous Email Validation
This example uses EnsureAsync to validate an email address asynchronously. It simulates an asynchronous check (for instance, validating the email format) before confirming that the email is acceptable.
async Task<Result<string>> ValidateEmailAsync(string email)
{
return await Task.FromResult(Result.Success(email))
.EnsureAsync(async e =>
{
// Simulate asynchronous work (e.g., calling an external service)
await Task.Delay(50);
return e.Contains("@") && e.Contains(".");
}, ResultError.New("Email format is invalid"));
}
await ValidateEmailAsync("bob@example.com")
.MatchAsync(
success: email => Console.WriteLine($"Email '{email}' is valid."),
failure: error => Console.WriteLine($"Email validation error: {error.Message}")
);
Example: Combined Synchronous and Asynchronous Number Validation
In this example, a number is first validated synchronously (it must be positive) and then asynchronously (it must be less than 100). This mixed pipeline demonstrates how you can chain both sync and async validations seamlessly.
async Task<Result<int>> ProcessNumberAsync(int number)
{
// Synchronous check: the number must be positive.
var result = Result.Success(number)
.Ensure(n => n > 0, ResultError.New("Number must be positive"))
// Asynchronous check: simulate delay and ensure the number is less than 100.
.EnsureAsync(async n =>
{
await Task.Delay(30);
return n < 100;
}, ResultError.New("Number must be less than 100"));
return result;
}
var processedResult = await ProcessNumberAsync(42)
.MatchAsync(
success: number => Console.WriteLine($"Processed number: {number}"),
failure: error => Console.WriteLine($"Processing error: {error.Message}")
);
Inspired by LanguageExt, this library offers a more compact and user-friendly alternative with extensive examples and tutorials.
There is a very detailed YouTube channel with a dedicated video tutorial playlist for this library.
Star this repository and follow me on GitHub to stay informed about new releases and updates. Your support fuels this project's growth!
If my content adds value to your projects, consider supporting me via crypto.
- Bitcoin: bc1qlfljm9mysdtu064z5cf4yq4ddxgdfztgvghw3w
- USDT(TRC20): TJFME9tAnwdnhmqGHDDG5yCs617kyQDV39
Thank you for being part of this community—let’s build smarter, together