Tethys.Results
1.4.0
dotnet add package Tethys.Results --version 1.4.0
NuGet\Install-Package Tethys.Results -Version 1.4.0
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Tethys.Results" Version="1.4.0" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Tethys.Results" Version="1.4.0" />
<PackageReference Include="Tethys.Results" />
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Tethys.Results --version 1.4.0
The NuGet Team does not provide support for this client. Please contact its maintainers for support.
#r "nuget: Tethys.Results, 1.4.0"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Tethys.Results@1.4.0
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Tethys.Results&version=1.4.0
#tool nuget:?package=Tethys.Results&version=1.4.0
The NuGet Team does not provide support for this client. Please contact its maintainers for support.
Tethys.Results
A lightweight, thread-safe Result pattern implementation for .NET that provides a clean, functional approach to error handling without exceptions.
Features
- ✅ Simple and Intuitive API - Easy to understand and use
- ✅ Thread-Safe - Immutable design ensures thread safety
- ✅ No Dependencies - Lightweight with zero external dependencies
- ✅ Async Support - First-class support for async/await patterns
- ✅ Functional Composition - Chain operations with
ThenandWhen - ✅ Type-Safe - Generic
Result<T>for operations that return values - ✅ Error Aggregation - Combine multiple results and aggregate errors
- ✅ Implicit Conversions - Seamless conversion between values and Results
Installation
dotnet add package Tethys.Results
Quick Start
Basic Usage
using Tethys.Results;
// Simple success/failure results
Result successResult = Result.Ok("Success message");
Result failureResult = Result.Fail("Something went wrong");
// Results with values
Result<string> valueResult = Result<string>.Ok("Hello World", "Operation completed");
Result<int> errorResult = Result<int>.Fail("Not found");
// Results with exceptions
var exception = new Exception("Test exception");
Result failWithException = Result.Fail("Error message", exception);
Chaining Operations
// Chain multiple operations that depend on each other
var result = Result.Ok("Start")
.Then(() =>
{
// Do some work
return Result.Ok("First operation completed");
})
.Then(() =>
{
// Do more work
return Result.Ok("Second operation completed");
})
.Then(() =>
{
// Final operation
return Result.Ok("All operations completed");
});
if (result.Success)
{
Console.WriteLine($"Success: {result.Message}");
}
else
{
Console.WriteLine($"Error: {result.Message}");
}
Working with Data
// Transform data through a pipeline
var result = Result<int>.Ok(42, "Initial value")
.Then(value =>
{
// Transform the value
return Result<string>.Ok(value.ToString(), "Converted to string");
})
.Then(str =>
{
// Further transformation
return Result<string>.Ok($"The answer is: {str}", "Formatted result");
});
// Extract the final value
string finalValue = result.GetValueOrDefault("No answer available");
Async Support
// Chain async operations seamlessly
var result = await Result.Ok("Start")
.ThenAsync(async () =>
{
await Task.Delay(100); // Simulate async work
return Result.Ok("Async operation completed");
})
.ThenAsync(async () =>
{
await Task.Delay(100); // More async work
return Result<string>.Ok("Final result", "All async operations done");
});
// Work with Task<Result> directly
Task<Result<int>> asyncResult = Task.FromResult(Result<int>.Ok(42, "From async"));
var processed = await asyncResult.ThenAsync(async value =>
{
await Task.Delay(100);
return Result<string>.Ok($"Processed: {value}", "Transformation complete");
});
Conditional Execution
// Execute operations conditionally
var result = Result.Ok("Initial state")
.When(true, () => Result.Ok("Condition was true"))
.When(false, () => Result.Ok("This won't execute"));
// Conditional execution with data
var dataResult = Result<int>.Ok(10)
.When(true, value => Result<int>.Ok(20))
.Then(value => Result<int>.Ok(value * 2)); // Results in 40
Error Aggregation
// Combine multiple validation results
var results = new List<Result>
{
ValidateEmail("user@example.com"),
ValidatePassword("SecurePass123!"),
ValidateUsername("johndoe")
};
var combined = Result.Combine(results);
if (!combined.Success)
{
// Access aggregated errors
var aggregateError = combined.Exception as AggregateError;
foreach (var error in aggregateError.ErrorMessages)
{
Console.WriteLine($"Validation error: {error}");
}
}
// Combine results with data
var dataResults = new List<Result<int>>
{
Result<int>.Ok(1),
Result<int>.Ok(2),
Result<int>.Ok(3)
};
var combinedData = Result<int>.Combine(dataResults);
if (combinedData.Success)
{
var sum = combinedData.Value.Sum(); // Sum is 6
}
Value Extraction
Result<User> userResult = GetUser(userId);
// Get value or default
User user = userResult.GetValueOrDefault(new User { Name = "Guest" });
// Try pattern
if (userResult.TryGetValue(out User foundUser))
{
Console.WriteLine($"Found user: {foundUser.Name}");
}
else
{
Console.WriteLine("User not found");
}
// Get value or throw (use sparingly)
try
{
User user = userResult.GetValueOrThrow();
// Use the user
}
catch (InvalidOperationException ex)
{
// Handle the error
Console.WriteLine($"Failed to get user: {ex.Message}");
}
Implicit Conversions
// Result<T> implicit conversions (unchanged)
Result<int> implicitResult = 42; // Creates Result<int>.Ok(42)
Result<string> stringResult = "Hello"; // Creates Result<string>.Ok("Hello")
// Result<TValue, TError> — use wrapper factories for unambiguous conversions
Result<StatusSynced, StepFailed> success = Result.Ok(new StatusSynced("txn-456"));
Result<StatusSynced, StepFailed> failure = Result.Fail(new StepFailed("step"));
// Works even when TValue == TError (no CS0457 compiler error)
Result<string, string> r1 = Result.Ok<string>("value");
Result<string, string> r2 = Result.Fail<string>("error");
// Method returns — intent is explicit at every call site
Result<Order, OrderError> ProcessOrder(OrderRequest req)
{
if (!req.IsValid)
return Result.Fail(new OrderError("Invalid request"));
return Result.Ok(new Order(req));
}
Chaining with Result<TValue, TError>
// Chain validation/processing steps with typed errors
Result<EfsTransaction, ReversalResult> pipeline = Validate(tracking)
.Then(txn => CheckWorkingDays(txn, 119))
.When(strategy == ReversalStrategy.RefundOnly, txn => ApplyRefund(txn))
.Then(txn => FinalizeReversal(txn));
// Transform the value type while keeping the error type fixed
Result<string, string> result = Result<int, string>.Ok(42)
.Then(v => Result<string, string>.Ok(v.ToString()))
.Map(s => $"The answer is: {s}");
// Map transforms the value without wrapping in Result
Result<string, string> mapped = Result<int, string>.Ok(5)
.Map(v => v.ToString());
// Async pipelines chain naturally
var asyncResult = await Result<int, string>.Ok(2)
.ThenAsync(async v =>
{
await Task.Delay(100);
return Result<int, string>.Ok(v + 3);
})
.ThenAsync(async v =>
{
await Task.Delay(100);
return Result<int, string>.Ok(v * 10);
});
// asyncResult.Value == 50
// Conditional async execution
var applyBonus = true;
var withBonus = await Result<int, string>.Ok(100)
.WhenAsync(applyBonus, async v =>
{
await Task.Delay(100);
return Result<int, string>.Ok(v * 2);
});
API Reference
Result Class
Result.Ok()- Creates a successful resultResult.Ok(string message)- Creates a successful result with a messageResult.Fail(string message)- Creates a failed result with an error messageResult.Fail(string message, Exception exception)- Creates a failed result with message and exceptionResult.FromException(Exception exception)- Creates a failed result from an exceptionResult.Combine(IEnumerable<Result> results)- Combines multiple results into one
Result<T> Class
Result<T>.Ok(T value)- Creates a successful result with a valueResult<T>.Ok(T value, string message)- Creates a successful result with a value and messageResult<T>.Fail(string message)- Creates a failed result with an error messageResult<T>.Fail(string message, Exception exception)- Creates a failed result with message and exceptionResult<T>.FromException(Exception exception)- Creates a failed result from an exceptionResult<T>.Combine(IEnumerable<Result<T>> results)- Combines multiple results with values
Extension Methods (Result / Result<T>)
Then(Func<Result> operation)- Chains operations on successful resultsThen<T>(Func<Result<T>> operation)- Chains operations that return valuesThenAsync(Func<Task<Result>> operation)- Chains async operationsThenAsync<T>(Func<Task<Result<T>>> operation)- Chains async operations that return valuesWhen(bool condition, Func<Result> operation)- Conditionally executes operationsGetValueOrDefault(T defaultValue = default)- Gets the value or a defaultTryGetValue(out T value)- Tries to get the value using the Try patternGetValueOrThrow()- Gets the value or throws an exception
Extension Methods (Result<TValue, TError>)
Then(Func<TValue, Result<TNewValue, TError>>)- Monadic bind, value type may changeWhen(bool, Func<TValue, Result<TValue, TError>>)- Conditional bindMap(Func<TValue, TNewValue>)- Transform value without Result wrappingThenAsync(Func<TValue, Task<Result<TNewValue, TError>>>)- Async bindThenAsynconTask<Result<TValue, TError>>- Chain from async sourceWhenAsync(bool, Func<TValue, Task<Result<TValue, TError>>>)- Async conditional bindWhenAsynconTask<Result<TValue, TError>>- Async conditional bind from async sourceMapAsync(Func<TValue, Task<TNewValue>>)- Async value transformationMapAsynconTask<Result<TValue, TError>>- Async value transformation from async source
Advanced Usage
Real-World Example: Order Processing
public async Task<Result<Order>> ProcessOrderAsync(OrderRequest request)
{
return await ValidateOrderRequest(request)
.ThenAsync(async validRequest => await CreateOrder(validRequest))
.ThenAsync(async order => await ApplyDiscounts(order))
.ThenAsync(async order => await CalculateTaxes(order))
.ThenAsync(async order => await ChargePayment(order))
.ThenAsync(async order => await SendConfirmationEmail(order))
.ThenAsync(async order =>
{
await LogOrderProcessed(order);
return Result<Order>.Ok(order, "Order processed successfully");
});
}
// Usage
var result = await ProcessOrderAsync(orderRequest);
if (result.Success)
{
return Ok(result.Value);
}
else
{
_logger.LogError(result.Exception, "Order processing failed: {Message}", result.Message);
return BadRequest(result.Message);
}
Integration with ASP.NET Core
[HttpGet("{id}")]
public async Task<IActionResult> GetUser(int id)
{
var result = await _userService.GetUserAsync(id);
return result.Success
? Ok(result.Value)
: NotFound(result.Message);
}
[HttpPost]
public async Task<IActionResult> CreateUser([FromBody] CreateUserRequest request)
{
var validationResult = ValidateRequest(request);
if (!validationResult.Success)
{
return BadRequest(validationResult.Message);
}
var result = await _userService.CreateUserAsync(request);
return result.Success
? CreatedAtAction(nameof(GetUser), new { id = result.Value.Id }, result.Value)
: BadRequest(result.Message);
}
Error Handling Patterns
// Centralized error handling
public Result<T> ExecuteWithErrorHandling<T>(Func<T> operation, string operationName)
{
try
{
var result = operation();
return Result<T>.Ok(result, $"{operationName} completed successfully");
}
catch (ValidationException ex)
{
return Result<T>.Fail($"Validation failed in {operationName}: {ex.Message}", ex);
}
catch (NotFoundException ex)
{
return Result<T>.Fail($"Resource not found in {operationName}: {ex.Message}", ex);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error in {OperationName}", operationName);
return Result<T>.Fail($"An unexpected error occurred in {operationName}", ex);
}
}
Best Practices
- Use Result for Expected Failures - Reserve exceptions for truly exceptional cases
- Chain Operations - Leverage
ThenandThenAsyncfor clean, readable code - Avoid Nested Results - Use
Theninstead of manual checking - Consistent Error Messages - Provide clear, actionable error messages
- Leverage Implicit Conversions - Simplify code with implicit conversions where appropriate
- Prefer TryGetValue - Use
TryGetValueoverGetValueOrThrowfor safer value extraction - Aggregate Validation Errors - Use
Result.Combinefor multiple validation checks
Contributing
We welcome contributions! Please see our Contributing Guide for details.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Support
- 📧 Email: support@tethys.dev
- 🐛 Issues: GitHub Issues
- 📖 Documentation: Full Documentation
Acknowledgments
Inspired by functional programming patterns and the Railway Oriented Programming approach.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
-
.NETStandard 2.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.