A comprehensive functional results library for .NET, providing robust error handling patterns and composition primitives
$ dotnet add package SharpUtils.ResultsA comprehensive functional results library for .NET 9 and C# 13, providing robust error handling patterns and composition primitives. This library helps eliminate exception-based error handling in favor of a more functional approach that's type-safe, composable, and easier to reason about.
Russian version (������� ������)
Results is a lightweight library implementing the Result pattern (also known as the Either monad in functional programming). It encapsulates the outcome of an operation that can either succeed with a value or fail with an error message, making error handling more explicit and predictable.
Install the package from NuGet:
dotnet add package SharpUtils.Results
Or via the NuGet Package Manager:
Install-Package SharpUtils.Results
Result<T>The core generic class representing an operation result:
Properties:
Value: The result value (when successful)ErrorMessage: The error details (when failed)IsSuccess: Boolean indicating successIsFailure: Boolean indicating failureFactory Methods:
Success(T value): Creates a successful result with a valueFailure(string errorMessage): Creates a failed result with an error messagePattern Matching:
Match<TResult>(Func<T, TResult> onSuccess, Func<string, TResult> onFailure): Handles both success and failure casesMatchAsync<TResult>(...): Asynchronous version of MatchTransformation:
Map<U>(Func<T, U> mapper): Transforms the value if successfulMapAsync<U>(Func<T, Task<U>> mapper): Asynchronously transforms the valueBind<U>(Func<T, Result<U>> binder): Chains results togetherBindAsync<U>(Func<T, Task<Result<U>>> binder): Asynchronously chains resultsCallbacks:
OnSuccess(Action<T> action): Executes an action if successfulOnFailure(Action<string> action): Executes an action if failedValue Extraction:
ValueOrDefault(T defaultValue): Safely extracts the value or returns a defaultOperator Overloads:
T to Result<T>Result<T> to bool (true if successful)Result<T> to T (throws if failed)Result (Non-generic)A specialization for operations that don't return a value:
Success(): Creates a successful result with no valueFailure(string errorMessage): Creates a failed result with an error messagePaginatedResult<T>Extends Result<IEnumerable<T>> with pagination metadata:
Properties:
Page: Current page numberPageSize: Number of items per pageTotalItems: Total number of items across all pagesTotalPages: Calculated total number of pagesFactory Methods:
Success(IEnumerable<T> value, uint page, uint pageSize, uint totalItems)Failure(string errorMessage)Empty(uint page, uint pageSize)Specialized Methods:
MapItems<U>(Func<T, U> mapper): Maps each item in the collectionMapItemsAsync<U>(...): Asynchronously maps itemsBindItems<U>(...): Binds each item to a new resultBindItemsAsync<U>(...): Asynchronously binds itemsUnitRepresents a void value, used in the non-generic Result implementation.
using Results;
// Creating successful and failed results
var success = Result<int>.Success(42);
var failure = Result<int>.Failure("Something went wrong");
// Checking result status
if (success.IsSuccess)
{
Console.WriteLine($"Value: {success.Value}");
}
// Using pattern matching
var message = success.Match(
value => $"The answer is {value}",
error => $"Error occurred: {error}"
);
// Using callbacks
success
.OnSuccess(value => Console.WriteLine($"Success: {value}"))
.OnFailure(error => Console.WriteLine($"Error: {error}"));
// Transforming results
var transformed = success
.Map(x => x * 2)
.Map(x => x.ToString());
// Chaining operations
var chained = success
.Bind(x => TryDivide(100, x))
.Bind(x => TryParse(x.ToString()));
// Using operator overloads
Result<int> implicitSuccess = 42; // Implicit conversion from value
bool isSuccessful = success; // Implicit conversion to bool
// Creating a paginated result
var pagedData = PaginatedResult<string>.Success(
new[] { "Item1", "Item2", "Item3" },
page: 1,
pageSize: 10,
totalItems: 25
);
// Accessing pagination information
Console.WriteLine($"Page {pagedData.Page} of {pagedData.TotalPages}");
Console.WriteLine($"Items: {pagedData.Value!.Count()} of {pagedData.TotalItems}");
// Transforming all items in the collection
var upperCaseItems = pagedData.MapItems(item => item.ToUpper());
// Binding items to other results
var boundItems = pagedData.BindItems(item =>
item.Length > 5
? Result<int>.Success(item.Length)
: Result<int>.Failure("Item too short")
);
// Async transformations
var asyncResult = await Result<int>.Success(42)
.MapAsync(async x => {
await Task.Delay(100);
return x * 2;
});
// Async binding
var asyncBound = await Result<string>.Success("42")
.BindAsync(async s => {
await Task.Delay(100);
if (int.TryParse(s, out var n))
return Result<int>.Success(n);
return Result<int>.Failure("Not a number");
});
This library supports the railway-oriented programming pattern, where functions can be composed together while handling errors gracefully:
// Define operations that return Results
Result<User> GetUser(string id) => /* ... */;
Result<Order> GetLatestOrder(User user) => /* ... */;
Result<Receipt> GenerateReceipt(Order order) => /* ... */;
Result<Unit> SendEmail(Receipt receipt, User user) => /* ... */;
// Chain operations, automatically handling failures
var result = GetUser("user123")
.Bind(GetLatestOrder)
.Bind(GenerateReceipt)
.Bind(receipt => SendEmail(receipt, user));
// Handle the final result
result.Match(
_ => Console.WriteLine("Email sent successfully"),
error => Console.WriteLine($"Failed: {error}")
);
Contributions are welcome! Please feel free to submit a Pull Request.
git checkout -b feature/amazing-feature)git commit -m 'Add some amazing feature')git push origin feature/amazing-feature)This project is licensed under the MIT License - see the LICENSE file for details.