7.8 KiB
StevanFreeborn.Results
A minimalistic, AOT-compatible Result type library with support for custom error types, for railway-oriented programming and functional error handling in .NET.
Features
- Railway-Oriented Programming: Chain operations that can fail without nested try-catch blocks
- Custom Error Types: Define your own error types that implement
IError - AOT Compatible: Works with Native AOT and trimming
- Nullable Reference Types: Full support for nullable reference types
- No External Dependencies: Lightweight with zero dependencies
- Comprehensive XML Documentation: Full IntelliSense support
Installation
Install via NuGet:
dotnet add package StevanFreeborn.Results
Quick Start
using StevanFreeborn.Results;
// Create a successful result
Result<Unit, Error> ok = Result<Unit, Error>.Ok(default);
// Create a failed result
Result<Unit, Error> fail = Result<Unit, Error>.Fail(new Error("NotFound", "User not found"));
// Work with results that have values
Result<int, Error> divisionResult = Divide(10, 2);
if (divisionResult.IsSuccess)
{
Console.WriteLine(divisionResult.Value); // 5
}
// Railway-oriented programming with Bind
Result<User, Error> GetUser(int id) => ...;
Result<Order, Error> GetOrder(int orderId) => ...;
// Chain operations without exceptions
Result<Order, Error> GetUserOrder(int userId, int orderId)
{
return GetUser(userId)
.Bind(user => GetOrder(user.OrderId))
.Map(order => order.WithUserDetails(user));
}
Custom Error Types
One of the key features is the ability to define your own error types:
// Define a custom error
public record DomainError(string Code, string Message) : IError;
// Use with Result
Result<User, DomainError> GetUser(int id)
{
if (id <= 0)
{
return Result<User, DomainError>.Fail(new DomainError("InvalidId", "User ID must be positive"));
}
// ... fetch user
return Result<User, DomainError>.Ok(user);
}
// Chain with custom errors
Result<Order, DomainError> result = GetUser(1)
.Bind(user => GetOrder(user.OrderId))
.MapError(e => new DomainError(e.Code, $"Failed to get order: {e.Message}"));
IError Interface
The IError interface is the foundation for custom error types:
public interface IError
{
string Code { get; }
string Message { get; }
}
Any type implementing IError can be used with Result<T, TError>.
Built-in Error Class
The library includes a built-in Error class that implements IError:
// Create an error
var error = new Error("NotFound", "User not found");
// With metadata
var errorWithMetadata = new Error(
"ValidationFailed", "Invalid input",
new Dictionary<string, object> { { "Field", "email" } }
);
Unit Type
The Unit struct represents a void-like type for use when no value is needed:
// For operations that only succeed or fail without a value
Result<Unit, Error> operation = DoSomething();
Result Types
Result<T, TError>
The main Result type with generic type parameters for both value and error:
// Creation
Result<int, Error> ok = Result<int, Error>.Ok(42);
Result<int, Error> fail = Result<int, Error>.Fail(new Error("Invalid", "Invalid input"));
// Check status
if (result.IsSuccess) { /* ... */ }
if (result.IsFailure) { /* ... */ }
// Access value (throws on failure)
int value = result.Value;
// Access error (throws on success)
Error error = result.Error;
Result without value
For operations that don't return a value, use Unit:
Result<Unit, Error> ok = Result<Unit, Error>.Ok(default);
Result<Unit, Error> fail = Result<Unit, Error>.Fail(new Error("Failed", "Something went wrong"));
Functional Operations
Map
Transforms the value if success, propagates the error if failure.
Result<int, Error> ok = Result<int, Error>.Ok(5);
Result<string, Error> mapped = ok.Map(x => x.ToString()); // Result<string, Error>.Ok("5")
MapError
Transforms the error if failure, propagates the value if success.
Result<int, Error> fail = Result<int, Error>.Fail(new Error("NotFound", "Not found"));
Result<int, Error> mapped = fail.MapError(e => new Error("Unexpected", e.Message));
Bind
Chains another operation that returns a Result.
Result<User, Error> GetUser(int id) => ...;
Result<Order, Error> GetOrder(int userId) => ...;
Result<Order, Error> GetUserOrder(int userId)
{
return GetUser(userId)
.Bind(user => GetOrder(user.Id));
}
Match
Executes different functions based on success or failure.
Result<int, Error> result = Divide(10, 2);
// Get a value
string message = result.Match(
onSuccess: value => $"Result: {value}",
onFailure: error => $"Error: {error.Message}"
);
// Execute actions
result.Match(
onSuccess: value => Console.WriteLine(value),
onFailure: error => Console.WriteLine(error.Message)
);
Try
Wraps a function that may throw an exception in a Result.
// Simple usage with default error handler
Result<string, Error> result = Result<string, Error>.Try(() => File.ReadAllText("file.txt"));
// Custom error handler
Result<string, Error> result = Result<string, Error>.Try(
() => File.ReadAllText("file.txt"),
ex => new Error("ReadError", ex.Message)
);
Async Operations
The library provides async extension methods for all functional operations.
// Async Map
Result<User, Error> user = await GetUserAsync(id);
Result<string, Error> userName = await user.MapAsync(u => GetNameAsync(u));
// Async Bind
Result<Order, Error> order = await GetUserAsync(id)
.BindAsync(user => GetOrderAsync(user.OrderId));
// Async Match
string result = await result.MatchAsync(
onSuccess: async value => await ProcessAsync(value),
onFailure: async error => await HandleErrorAsync(error)
);
// Async Try
Result<string, Error> result = await ResultAsyncExtensions.TryAsync(
() => HttpClient.GetStringAsync("https://api.example.com")
);
Extension Methods
Result<T, TError> Methods
| Method | Description |
|---|---|
Map(Func<T, TNew>) |
Transforms the value |
Map(Func<T, TNew>, Func<TError, TNewError>) |
Transforms the value or error |
MapError(Func<TError, TNewError>) |
Transforms the error |
Bind(Func<T, Result<TNew, TError>>) |
Chains a new Result |
Match(Func<T, TResult>, Func<TError, TResult>) |
Pattern matching returning a value |
Match(Action<T>, Action<TError>) |
Pattern matching executing actions |
Try(Func<T>) |
Wraps a function that may throw |
Async Extensions
| Method | Description |
|---|---|
MapAsync |
Async version of Map |
MapErrorAsync |
Async version of MapError |
BindAsync |
Async version of Bind |
MatchAsync |
Async version of Match |
TryAsync |
Async version of Try |
Requirements
- .NET Standard 2.1
- .NET 10.0+
License
MIT License - see LICENSE.md for details.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.