BYSResults 1.2.3

dotnet add package BYSResults --version 1.2.3
                    
NuGet\Install-Package BYSResults -Version 1.2.3
                    
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="BYSResults" Version="1.2.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="BYSResults" Version="1.2.3" />
                    
Directory.Packages.props
<PackageReference Include="BYSResults" />
                    
Project file
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 BYSResults --version 1.2.3
                    
#r "nuget: BYSResults, 1.2.3"
                    
#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 BYSResults@1.2.3
                    
#: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=BYSResults&version=1.2.3
                    
Install as a Cake Addin
#tool nuget:?package=BYSResults&version=1.2.3
                    
Install as a Cake Tool

BYSResults

NuGet version License

Lightweight result types for explicit success/failure handling in .NET applications.


Table of Contents

  1. Features
  2. Choosing Between Result and Result<T>
  3. Installation
  4. Quick Start / Usage Examples
  5. Real-World Examples
  6. API Reference
  7. Advanced Usage
  8. Thread Safety
  9. Revision History
  10. Contributing
  11. License
  12. Authors & Acknowledgments
  13. Links

Features

  • No exceptions for control flow — all outcomes are explicit
  • Fluent chaining via Bind, Map, MapAsync, BindAsync, etc.
  • Pattern matching with Match for elegant error handling
  • Exception safety with Try and TryAsync factory methods
  • Value extraction with GetValueOr and OrElse for fallback handling
  • Side effects via Tap, TapAsync, OnSuccess, OnFailure without breaking chains
  • Validation with Ensure for 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 (Result vs. 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:

  1. Web API Examples - Converting Result to HTTP responses, CRUD operations
  2. Database Examples - Repository pattern, transactions, batch operations
  3. Validation Examples - Form validation, error aggregation, business rules
  4. Async Examples - External APIs, parallel operations, retry logic
  5. 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 Result instances 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 as Result).
  • 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. Use Result.Combine(...) from the base class instead, which accepts both Result and Result<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 .Value on 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() and AddErrors() methods modify the internal error list
  • These mutations are NOT thread-safe
  • Concurrent calls to AddError() from multiple threads can cause race conditions

Recommendations:

  1. 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 threads
    
  2. Don'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 SAFE
    
  3. Synchronize 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

  1. Fork the repository.

  2. Create a feature branch:

    git checkout -b feature/YourFeature
    
  3. Commit your changes:

    git commit -m "Add awesome feature"
    
  4. Push to the branch:

    git push origin feature/YourFeature
    
  5. Open a Pull Request.

  6. 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

@Thumper631

Thanks to all contributors.


Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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.

Version Downloads Last Updated
1.2.3 287 11/3/2025
1.2.2 176 11/3/2025
1.2.1 118 10/31/2025
1.2.0 183 10/29/2025
1.1.5 282 9/30/2025
1.1.4 332 6/2/2025
1.1.3 177 6/1/2025
1.1.2 175 6/1/2025
1.1.1 164 6/1/2025
1.1.0 159 6/1/2025
1.0.0 360 5/8/2025

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.