BYSResults 1.2.3
dotnet add package BYSResults --version 1.2.3
NuGet\Install-Package BYSResults -Version 1.2.3
<PackageReference Include="BYSResults" Version="1.2.3" />
<PackageVersion Include="BYSResults" Version="1.2.3" />
<PackageReference Include="BYSResults" />
paket add BYSResults --version 1.2.3
#r "nuget: BYSResults, 1.2.3"
#:package BYSResults@1.2.3
#addin nuget:?package=BYSResults&version=1.2.3
#tool nuget:?package=BYSResults&version=1.2.3
BYSResults
Lightweight result types for explicit success/failure handling in .NET applications.
Table of Contents
- Features
- Choosing Between Result and Result<T>
- Installation
- Quick Start / Usage Examples
- Real-World Examples
- API Reference
- Advanced Usage
- Thread Safety
- Revision History
- Contributing
- License
- Authors & Acknowledgments
- Links
Features
- No exceptions for control flow — all outcomes are explicit
- Fluent chaining via
Bind,Map,MapAsync,BindAsync, etc. - Pattern matching with
Matchfor elegant error handling - Exception safety with
TryandTryAsyncfactory methods - Value extraction with
GetValueOrandOrElsefor fallback handling - Side effects via
Tap,TapAsync,OnSuccess,OnFailurewithout breaking chains - Validation with
Ensurefor inline condition checking - Async support for modern .NET applications
- Easy combination with
Result.Combine(...) - Error aggregation and inspection (
.Errors,.FirstError) - Generic and non-generic variants (
Resultvs.Result<T>)
Choosing Between Result and Result<T>
BYSResults provides two result types to match different operation scenarios. Understanding when to use each will make your code clearer and more intentional.
Quick Decision Rule
Use Result when your operation only needs to indicate success or failure (no return value needed):
public Result DeleteUser(int userId)
public Result SendEmail(string to, string subject)
public Result ValidatePassword(string password)
Use Result<T> when your operation returns a value on success:
public Result<User> GetUserById(int userId)
public Result<int> ParseInteger(string input)
public Result<decimal> CalculateTotal(Order order)
Common Scenarios
| Scenario | Type | Example Signature |
|---|---|---|
| Delete operation | Result |
Result DeleteCustomer(int id) |
| Update (no return) | Result |
Result UpdateSettings(Settings settings) |
| Validation (pass/fail) | Result |
Result ValidateEmail(string email) |
| Send/publish | Result |
Result PublishMessage(Message msg) |
| Fetch/get | Result<T> |
Result<User> GetUser(int id) |
| Create (return entity) | Result<T> |
Result<Order> CreateOrder(OrderDto dto) |
| Parse/transform | Result<T> |
Result<int> ParseInt(string s) |
| Calculate | Result<T> |
Result<decimal> CalculatePrice(Item item) |
Feature Availability
Result<T> includes additional functional programming methods that work with values:
// Result<T> has value transformation methods
var result = Result<int>.Success(42)
.Map(x => x * 2) // Transform the value
.Bind(x => Divide(x, 2)) // Chain operations
.Ensure(x => x > 0, "Must be positive");
// Result doesn't need these because there's no value to transform
var result = Result.Success()
.Ensure(() => IsValid(), "Must be valid")
.Tap(() => LogSuccess());
Practical Examples
Non-generic for void-like operations:
// Repository delete method - no value to return
public async Task<Result> DeleteProductAsync(int productId)
{
var product = await _context.Products.FindAsync(productId);
if (product == null)
return Result.Failure("PRODUCT_NOT_FOUND", "Product not found");
_context.Products.Remove(product);
await _context.SaveChangesAsync();
return Result.Success(); // Just confirms deletion succeeded
}
Generic for data-returning operations:
// Repository get method - returns the product
public async Task<Result<Product>> GetProductAsync(int productId)
{
var product = await _context.Products.FindAsync(productId);
return product == null
? Result<Product>.Failure("PRODUCT_NOT_FOUND", "Product not found")
: Result<Product>.Success(product); // Returns the found product
}
Design Tip
Think of Result as analogous to void return types, and Result<T> as analogous to typed return values:
// Traditional approach
void DeleteUser(int id) → Result DeleteUser(int id)
User GetUser(int id) → Result<User> GetUser(int id)
The key difference is that both Result and Result<T> can represent failure with error details, unlike traditional return types.
Installation
Install via .NET CLI:
dotnet add package BYSResults
Or via Package Manager Console:
Install-Package BYSResults
Quick Start / Usage Examples
using BYSResults;
// Simple success/failure without a value
var r1 = Result.Success();
var r2 = Result.Failure("E001", "Something went wrong");
if (r2.IsFailure)
{
Console.WriteLine(r2.FirstError?.Message);
}
// Generic result with a value
var r3 = Result<int>.Success(42);
var r4 = Result<string>.Failure("Missing data");
if (r3.IsSuccess)
{
Console.WriteLine($"Value is {r3.Value}");
}
// Inspect errors
foreach (var err in r4.Errors)
{
Console.WriteLine(err);
}
Real-World Examples
For comprehensive, runnable examples demonstrating real-world usage patterns, see the BYSResults.Examples project.
Example Categories
The examples project includes:
- Web API Examples - Converting Result to HTTP responses, CRUD operations
- Database Examples - Repository pattern, transactions, batch operations
- Validation Examples - Form validation, error aggregation, business rules
- Async Examples - External APIs, parallel operations, retry logic
- Chaining Examples - Railway-oriented programming, complex workflows
Web API Integration
// Converting Result to HTTP-style responses
public ApiResponse<User> GetUser(int id)
{
var result = userService.GetUserById(id);
return result.Match(
onSuccess: user => new ApiResponse<User>
{
StatusCode = 200,
Data = user
},
onFailure: errors => new ApiResponse<User>
{
StatusCode = 404,
Errors = errors.Select(e => e.Message).ToList()
}
);
}
Database Operations
// Repository pattern with validation and side effects
public async Task<Result<Customer>> CreateCustomerAsync(CustomerDto dto)
{
return await Result<CustomerDto>.Success(dto)
.Ensure(d => !string.IsNullOrEmpty(d.Email), "Email is required")
.Ensure(d => d.Email.Contains("@"), "Valid email is required")
.Ensure(d => d.Age >= 18, new Error("AGE_RESTRICTION", "Must be 18 or older"))
.MapAsync(async d => new Customer
{
Name = d.Name,
Email = d.Email,
Age = d.Age,
CreatedAt = DateTime.UtcNow
})
.BindAsync(async customer => await repository.SaveAsync(customer))
.TapAsync(async customer => await SendWelcomeEmailAsync(customer));
}
Validation Patterns
// Collecting all validation errors at once
public Result<RegistrationForm> ValidateRegistration(RegistrationForm form)
{
var errors = new List<Error>();
if (string.IsNullOrWhiteSpace(form.Name))
errors.Add(new Error("NAME_REQUIRED", "Name is required"));
if (string.IsNullOrWhiteSpace(form.Email))
errors.Add(new Error("EMAIL_REQUIRED", "Email is required"));
else if (!form.Email.Contains("@"))
errors.Add(new Error("EMAIL_INVALID", "Email must be valid"));
if (form.Password.Length < 8)
errors.Add(new Error("PASSWORD_TOO_SHORT", "Password must be 8+ characters"));
return errors.Any()
? Result<RegistrationForm>.Failure(errors)
: Result<RegistrationForm>.Success(form);
}
Railway-Oriented Programming
// Complex multi-stage workflow with early exit on failure
public async Task<Result<ProcessedOrder>> ProcessOrderAsync(OrderRequest request)
{
return await Result<OrderRequest>.Success(request)
.BindAsync(async r => await ValidateOrderAsync(r))
.BindAsync(async r => await CheckInventoryAsync(r))
.BindAsync(async r => await CalculatePricingAsync(r))
.BindAsync(async r => await ProcessPaymentAsync(r))
.BindAsync(async r => await CreateShipmentAsync(r))
.TapAsync(async o => await SendConfirmationEmailAsync(o))
.TapAsync(async o => await LogOrderAsync(o));
}
External API Integration
// Handling external API calls with error handling
public async Task<Result<WeatherData>> GetWeatherAsync(string city)
{
return await Result<string>.Success(city)
.Ensure(c => !string.IsNullOrWhiteSpace(c), "City is required")
.MapAsync(async c => await FetchWeatherJsonAsync(c))
.MapAsync(async json => await DeserializeWeatherAsync(json))
.Ensure(data => data != null, "Failed to parse weather data")
.Ensure(data => data.Temperature > -100 && data.Temperature < 100,
"Invalid temperature reading");
}
Fallback Chains
// Try primary source, fall back to cache, then default
public async Task<Result<Configuration>> LoadConfigurationAsync()
{
return await LoadFromRemoteAsync()
.OrElse(async () => await LoadFromDatabaseAsync())
.OrElse(async () => await LoadFromFileAsync())
.OrElse(() => Result<Configuration>.Success(GetDefaultConfiguration()));
}
Retry Logic
// Automatic retry with exponential backoff
public async Task<Result<T>> RetryAsync<T>(
Func<Task<Result<T>>> operation,
int maxRetries = 3)
{
Result<T>? lastResult = null;
for (int attempt = 1; attempt <= maxRetries; attempt++)
{
lastResult = await operation();
if (lastResult.IsSuccess)
return lastResult;
Console.WriteLine($"Attempt {attempt} failed. Retrying...");
await Task.Delay(TimeSpan.FromSeconds(attempt)); // Exponential backoff
}
return lastResult!.AddError(
new Error("MAX_RETRIES", $"Failed after {maxRetries} attempts"));
}
Conditional Processing
// Multi-level approval workflow
public Result<ApprovalResult> ProcessApprovalWorkflow(ApprovalRequest request)
{
return Result<ApprovalRequest>.Success(request)
.Ensure(r => r.Amount > 0, "Amount must be positive")
.Bind(r => r.Amount < 1000
? Result<ApprovalRequest>.Success(r) // Auto-approve
: GetManagerApproval(r))
.Bind(r => r.Amount < 5000
? Result<ApprovalRequest>.Success(r)
: GetDirectorApproval(r))
.Map(r => new ApprovalResult
{
RequestId = r.RequestId,
Approved = true
});
}
Running the Examples
cd BYSResults.Examples
dotnet run
See the Examples README for detailed explanations of each pattern.
API Reference
This section provides detailed API documentation for both Result and Result<T>. For guidance on choosing between the two types, see Choosing Between Result and Result<T>.
Quick reminder:
Result- For operations that only indicate success/failure (no return value)Result<T>- For operations that return a value on success
Result
Core Properties & Status
- bool IsSuccess - True if operation succeeded.
- bool IsFailure - True if operation failed.
- IReadOnlyList<Error> Errors - List of all errors (empty if success).
- Error? FirstError - Shortcut to the first error, or null if none.
Factory Methods
- static Result Success() - Create a successful
Result. - static Result Failure(...) - Create a failure
Result(overloads:Error,IEnumerable<Error>,string,(code, message)). - static Result Try(Action) - Execute an action and return success, or failure if exception is thrown.
- static Result Combine(...) - Combine multiple
Resultinstances into one, aggregating errors.
Error Management
- Result AddError(Error) - Add an error to an existing
Result. - Result AddError(Exception) - Add an exception as an error to an existing
Result. - Result AddErrors(...) - Add multiple errors to an existing
Result.
Control Flow & Pattern Matching
- void Match(Action onSuccess, Action<IReadOnlyList<Error>> onFailure) - Execute one of two actions based on result state.
- TReturn Match<TReturn>(...) - Execute one of two functions and return a value based on result state.
- Result OnSuccess(Func<Result>) - Execute a function and return its result if successful.
- Result OnFailure(Func<IReadOnlyList<Error>, Result>) - Execute a function and return its result if failed.
Side Effects & Debugging
- Result Tap(Action) - Execute an action without modifying the result (useful for logging).
- Result TapOnSuccess(Action) - Execute an action only if successful.
- Result TapOnFailure(Action<IReadOnlyList<Error>>) - Execute an action only if failed.
Validation
- Result Ensure(Func<bool>, Error) - Validate a condition, adding an error if false.
- Result Ensure(Func<bool>, string) - Validate a condition, adding an error message if false.
Result<T>
Core Properties
- T? Value - The value if successful (default if failure).
Factory Methods
- static Result<T> Success(T value) - Create a successful
Result<T>with the given value. - static Result<T> Failure(...) - Create a failure
Result<T>(same overloads asResult). - static Result<T> Try(Func<T>) - Execute a function and return its value, or failure if exception is thrown.
Note:
Result<T>.Combine()was removed in v1.2.1. UseResult.Combine(...)from the base class instead, which accepts bothResultandResult<T>instances.
Functional Composition
- Result<TNext> Map(Func<T, TNext>) - Transform the value on success, propagate errors on failure.
- Result<TNext> Bind(Func<T, Result<TNext>>) - Chain operations that return
Result<TNext>.
Async Operations
- Task<Result<TNext>> MapAsync(Func<T, Task<TNext>>) - Asynchronously transform the value.
- Task<Result<TNext>> BindAsync(Func<T, Task<Result<TNext>>>) - Asynchronously chain operations.
- static Task<Result<T>> TryAsync(Func<Task<T>>) - Execute async function, catching exceptions.
- Task<Result<T>> TapAsync(Func<T, Task>) - Execute async side effect.
Value Extraction & Fallbacks
- T GetValueOr(T defaultValue) - Get value if successful, otherwise return default.
- T GetValueOr(Func<T>) - Get value if successful, otherwise call function for default.
- Result<T> OrElse(Result<T>) - Return this if successful, otherwise return alternative.
- Result<T> OrElse(Func<Result<T>>) - Return this if successful, otherwise call function for alternative.
Error Management
- Result<T> WithValue(T) - Set the
.Valueon an existing successful result. - new Result<T> AddError(Error) - Add an error (returns
Result<T>for chaining). - new Result<T> AddErrors(...) - Add multiple errors (returns
Result<T>).
Control Flow & Pattern Matching
- void Match(Action<T>, Action<IReadOnlyList<Error>>) - Execute one of two actions based on result state.
- TReturn Match<TReturn>(Func<T, TReturn>, Func<IReadOnlyList<Error>, TReturn>) - Execute function and return value.
- Result<T> OnSuccess(Func<T, Result<T>>) - Execute a function with the value if successful.
- Result<T> OnFailure(Func<IReadOnlyList<Error>, Result<T>>) - Execute a function if failed.
Side Effects & Debugging
- Result<T> Tap(Action<T>) - Execute an action with the value without modifying result.
- new Result<T> TapOnFailure(Action<IReadOnlyList<Error>>) - Execute an action only if failed.
Validation
- Result<T> Ensure(Func<T, bool>, Error) - Validate the value, adding an error if condition is false.
- Result<T> Ensure(Func<T, bool>, string) - Validate the value, adding an error message if condition is false.
Error
string Code Error code (optional).
string Message Human-readable error message.
override string ToString() Returns
"Code: Message"or just"Message"if no code.Equality operators
==,!=for value equality.
Advanced Usage
Pattern Matching
// Handle both success and failure cases
var result = Result<int>.Try(() => int.Parse(input));
var message = result.Match(
onSuccess: value => $"Parsed: {value}",
onFailure: errors => $"Failed: {errors.First().Message}"
);
Exception Safety with Try
// Synchronous
var result = Result<int>.Try(() => RiskyOperation());
// Asynchronous
var asyncResult = await Result<string>.TryAsync(async () => await FetchDataAsync());
Value Extraction with Fallbacks
// Simple fallback
int value = result.GetValueOr(0);
// Lazy fallback
int value = result.GetValueOr(() => ExpensiveDefault());
// Alternative result
var final = primaryResult.OrElse(fallbackResult);
Fluent Chaining & Validation
var result = Result<int>.Success(42)
.Ensure(v => v > 0, "Must be positive")
.Ensure(v => v < 100, "Must be less than 100")
.Map(v => v * 2)
.Tap(v => Console.WriteLine($"Value: {v}"))
.Bind(v => AnotherOperation(v));
Async Operations
var result = await Result<User>.Success(userId)
.MapAsync(async id => await GetUserAsync(id))
.BindAsync(async user => await ValidateUserAsync(user))
.TapAsync(async user => await LogAsync($"User: {user.Name}"));
Error Recovery
var result = Result<int>.Failure("Database error")
.OnFailure(errors =>
{
Logger.Log(errors);
return Result<int>.Success(GetCachedValue());
});
Combining Results
var combined = Result.Combine(
ValidateName(name),
ValidateEmail(email),
ValidateAge(age)
);
if (combined.IsFailure)
{
Console.WriteLine($"Validation failed: {string.Join(", ", combined.Errors)}");
}
Thread Safety
Overview
BYSResults is designed with immutability and thread safety in mind, but there are important considerations when sharing Result instances across threads.
Thread-Safe Operations
Immutable Components:
- Error instances are fully immutable and thread-safe
- All properties are readonly
- Can be safely shared across threads
Reading Results:
- Reading properties (
IsSuccess,IsFailure,Value,Errors) is thread-safe - Once a Result is created, its success/failure state doesn't change
Thread-Unsafe Operations
Mutable Error Lists:
AddError()andAddErrors()methods modify the internal error list- These mutations are NOT thread-safe
- Concurrent calls to
AddError()from multiple threads can cause race conditions
Recommendations:
Avoid Mutation After Creation (Preferred)
// Good: Create result with all errors upfront var errors = new List<Error> { error1, error2 }; var result = Result<int>.Failure(errors); // Now safe to share across threadsDon't Share Mutable Results
// Avoid: Sharing a result while adding errors var result = Result.Success(); Task.Run(() => result.AddError(error1)); // NOT SAFE Task.Run(() => result.AddError(error2)); // NOT SAFESynchronize Mutations
// If you must mutate, use synchronization var result = Result.Success(); var lockObj = new object(); lock (lockObj) { result.AddError(error1); result.AddError(error2); }
Best Practices for Concurrent Code
Pattern 1: Immutable Results
// Create results immutably and share freely
public async Task<Result<Data>> ProcessAsync()
{
var result = await FetchDataAsync();
// Once created, safe to share
return result;
}
Pattern 2: Collect Then Create
// Collect errors locally, then create result once
public Result ValidateConcurrently(IEnumerable<Item> items)
{
var errors = new ConcurrentBag<Error>();
Parallel.ForEach(items, item =>
{
if (!IsValid(item))
errors.Add(new Error($"Invalid: {item.Name}"));
});
return errors.Any()
? Result.Failure(errors)
: Result.Success();
}
Pattern 3: Task Results
// Each task creates its own result
public async Task<Result> ProcessMultipleAsync(IEnumerable<Item> items)
{
var tasks = items.Select(ProcessItemAsync);
var results = await Task.WhenAll(tasks);
// Combine results (thread-safe operation)
return Result.Combine(results);
}
Summary
| Operation | Thread-Safe? | Notes |
|---|---|---|
| Reading properties | ✓ Yes | Always safe once created |
| Creating new results | ✓ Yes | Factory methods are safe |
Map, Bind, etc. |
✓ Yes | Return new instances |
AddError() |
✗ No | Mutates internal list |
AddErrors() |
✗ No | Mutates internal list |
Combine() |
✓ Yes | Reads only, creates new result |
General Rule: Treat Result instances as immutable after creation. If you need to add errors, create the result with all errors upfront or ensure proper synchronization.
Revision History
For a detailed changelog with all releases and changes, see CHANGELOG.md.
Latest Release: v1.2.3 (2025-11-02)
- Added "Choosing Between Result and Result<T>" guidance section
- Enhanced XML documentation for Result and Result<T> classes
- Improved API Reference introduction with usage guidance
Contributing
Fork the repository.
Create a feature branch:
git checkout -b feature/YourFeatureCommit your changes:
git commit -m "Add awesome feature"Push to the branch:
git push origin feature/YourFeatureOpen a Pull Request.
Follow the code style and include tests.
See CONTRIBUTING.md for more details.
License
Licensed under the MIT License. See LICENSE for details.
Authors & Acknowledgments
Thanks to all contributors.
Links
- NuGet: https://www.nuget.org/packages/BYSResults
- Repository: https://github.com/Thumper631/BYSResults
- Issues: https://github.com/Thumper631/BYSResults/issues
- Documentation: https://Thumper631.github.io/BYSResults
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. 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. |
-
net8.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.
1.2.3 - Documentation improvements: Added "Choosing Between Result and Result<T>" guidance section, enhanced XML documentation for both classes, improved API Reference introduction
1.2.2 - Documentation improvements: Added comprehensive Thread Safety section, CONTRIBUTING.md, CHANGELOG.md, .editorconfig, GitHub issue/PR templates, aligned copyright statements
1.2.1 - Code quality improvements: Fixed AddError(Exception) to use exception type as error code, improved inner exception formatting, removed Result<T>.Combine() method, modernized Error.GetHashCode(), updated documentation
1.2.0 - Major feature release: Match pattern matching, Try/TryAsync exception safety, GetValueOr/OrElse, Tap/TapAsync, OnSuccess/OnFailure, Ensure validation, async operations (MapAsync/BindAsync/TapAsync), comprehensive test suite
1.1.5 - Fixed NuGet package health issues (enabled deterministic builds, added symbols)
1.1.4 - Updated GetInnerException to handle null InnerException
1.1.3 - Added Revision History to readme.md
1.1.1/2 - Added readme.md file
1.1.0 - Added AddError(Exception exception)
1.0.0 - Initial release of BYSResults.