NextChapterDigital.Results 1.0.1

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

Result Pattern Library

A robust, type-safe implementation of the Result pattern for .NET 8.0, providing functional error handling without exceptions.

Build and Test Publish Packages NuGet NuGet Downloads .NET 8.0 License

Overview

This library implements the Result pattern (also known as Either monad) to handle operations that can succeed or fail, replacing exception-based error handling with explicit, type-safe error management.

Key Benefits:

  • 🎯 Type-safe - Compile-time guarantee that errors are handled
  • 🚀 Performance - No exception overhead for business logic errors
  • 📦 Composable - Chain operations with Map, Bind, and LINQ
  • 🔍 Traceable - Built-in error location tracking with caller information
  • 🎨 Expressive - Clean, readable code with functional patterns
  • 🌐 Serializable - Full JSON support for API responses

Installation

# Via NuGet (once published)
dotnet add package NextChapterDigital.Results

# Or add to your .csproj
<PackageReference Include="NextChapterDigital.Results" Version="1.0.0" />

Quick Start

Basic Usage

using NextChapterDigital.Results;
using NextChapterDigital.Results.Errors;

// Success case - multiple ways to create
Result<int> success = Result.Success(42);
Result<int> implicitSuccess = 42;  // Implicit conversion

// Error case
Result<int> error = new Error
{
    Category = ErrorCategory.ValidationFailed,
    Reason = "Value must be positive",
    Source = "Calculator"
};

// Pattern matching to handle both cases
var output = success.Match(
    onSuccess: value => $"Result: {value}",
    onError: error => $"Error: {error.Reason}"
);

Real-World Example

public class UserService
{
    public async Task<Result<User>> CreateUserAsync(CreateUserRequest request)
    {
        return await ValidateRequest(request)
            .Bind(HashPassword)
            .BindAsync(SaveUserAsync)
            .TapAsync(user => LogUserCreatedAsync(user))
            .MapAsync(user => AddDefaultPermissions(user));
    }

    private Result<CreateUserRequest> ValidateRequest(CreateUserRequest request)
    {
        if (string.IsNullOrEmpty(request.Email))
            return Error.Create().ValidationFailed("Email is required", "UserService");

        if (request.Password.Length < 8)
            return Error.Create().ValidationFailed("Password must be at least 8 characters", "UserService");

        return request;
    }

    private Result<CreateUserRequest> HashPassword(CreateUserRequest request)
    {
        try
        {
            request.Password = _hasher.Hash(request.Password);
            return request;
        }
        catch (Exception ex)
        {
            return Error.Create(ex).General("Failed to hash password", "UserService");
        }
    }

    private async Task<Result<User>> SaveUserAsync(CreateUserRequest request)
    {
        var user = await _repository.SaveAsync(request);
        return user ?? Error.Create().General("Failed to save user", "UserService");
    }
}

Core Concepts

Result Types

Result<T> - Generic Result

Represents an operation that returns a value on success or an error on failure.

Result<string> GetUserName(int userId)
{
    var user = _repository.FindUser(userId);

    if (user == null)
        return Error.Create().NotFound($"User {userId} not found", "UserService");

    return user.Name;  // Implicit conversion
}
Result - Non-Generic Result

Represents an operation that succeeds or fails without returning a value.

Result DeleteUser(int userId)
{
    var user = _repository.FindUser(userId);

    if (user == null)
        return Error.Create().NotFound($"User {userId} not found", "UserService");

    _repository.Delete(user);
    return Result.Success();
}

Error System

Built-in Error Categories
ErrorCategory.General           // General errors (1000)
ErrorCategory.NotFound          // Resource not found (1001)
ErrorCategory.NoResults         // No results returned (1002)
ErrorCategory.Unauthorized      // Authorization required (1003)
ErrorCategory.Conflict          // Conflict occurred (1004)
ErrorCategory.ValidationFailed  // Validation failed (1005)
Creating Errors with Location Tracking
// Automatic caller information tracking
var error = Error.Create().NotFound("User not found", "UserService");

// Error will contain:
// - Member: Method name where Error.Create() was called
// - FilePath: Source file path
// - LineNumber: Line number
// - Exception: Optional exception reference

// Custom properties
var error = Error.Create().ValidationFailed(
    "Invalid email format",
    "EmailValidator",
    new Dictionary<string, object?>
    {
        ["Email"] = email,
        ["ValidationRule"] = "EmailFormat"
    }
);
Custom Error Categories
public static class CustomErrors
{
    public static readonly ErrorCategory PaymentFailed =
        ErrorCategory.General.SubCategory(
            "PaymentFailed",
            "domain/payment-failed",
            "Payment processing failed"
        );

    public static Error InsufficientFunds(this ErrorLocation location, decimal required)
    {
        return location.Custom(
            PaymentFailed,
            $"Insufficient funds. Required: {required:C}",
            "PaymentService",
            new Dictionary<string, object?> { ["RequiredAmount"] = required }
        );
    }
}

Functional Operations

Map - Transform Success Values

Result<int> number = Result.Success(42);
Result<string> text = number.Map(n => n.ToString());  // Result<string> = "42"

// Async version
Result<string> textAsync = await number.MapAsync(async n =>
{
    await LogAsync(n);
    return n.ToString();
});

Bind - Chain Operations

Result<string> input = Result.Success("123");

Result<string> result = input
    .Bind(ParseInt)              // Result<int>
    .Bind(ValidatePositive)      // Result<int>
    .Map(n => n * 2)             // Result<int>
    .Map(n => n.ToString());     // Result<string>

Result<int> ParseInt(string s) =>
    int.TryParse(s, out var n)
        ? n
        : Error.Create().ValidationFailed("Invalid integer", "Parser");

Result<int> ValidatePositive(int n) =>
    n > 0
        ? n
        : Error.Create().ValidationFailed("Must be positive", "Validator");

Tap - Side Effects

// Execute logging without changing the result
var result = await GetUserAsync(userId)
    .Tap(user => _logger.LogInformation("User found: {Name}", user.Name))
    .TapError(error => _logger.LogError("User lookup failed: {Reason}", error.Reason))
    .MapAsync(user => EnrichUserDataAsync(user));

LINQ Query Syntax

var result = from user in GetUser(userId)
             from permissions in GetUserPermissions(user.Id)
             from profile in GetUserProfile(user.Id)
             select new UserViewModel
             {
                 User = user,
                 Permissions = permissions,
                 Profile = profile
             };

// Equivalent to:
var result = GetUser(userId)
    .Bind(user => GetUserPermissions(user.Id)
        .Bind(permissions => GetUserProfile(user.Id)
            .Map(profile => new UserViewModel
            {
                User = user,
                Permissions = permissions,
                Profile = profile
            })));

Advanced Patterns

Exception Wrapping

// Convert exceptions to Results
var result = await FetchDataAsync()
    .ToResult(location => Error.Create(location.Exception).General(
        "Failed to fetch data",
        "DataService"
    ));

// Simple version with default error
var result = await FetchDataAsync().ToResult();

Collection Processing

// All-or-nothing: Returns first error or list of all successes
var userIds = new[] { 1, 2, 3, 4, 5 };
Result<List<User>> users = userIds
    .Select(id => GetUser(id))
    .Flatten();

users.Switch(
    onSuccess: list => Console.WriteLine($"Loaded {list.Count} users"),
    onError: error => Console.WriteLine($"Failed: {error.Reason}")
);

Async Pipelines

public async Task<Result<OrderConfirmation>> ProcessOrderAsync(Order order)
{
    return await ValidateOrder(order)
        .BindAsync(CheckInventoryAsync)
        .BindAsync(ProcessPaymentAsync)
        .TapAsync(order => NotifyWarehouseAsync(order))
        .MapAsync(CreateConfirmationAsync)
        .TapAsync(SendEmailAsync)
        .TapErrorAsync(error => LogErrorAsync(error));
}

Partial Success Handling

public async Task<PartialResult<T>> ProcessItemsWithPartialSuccessAsync<T>(List<T> items)
{
    var results = await Task.WhenAll(items.Select(ProcessItemAsync));

    return new PartialResult<T>
    {
        Successes = results.Where(r => r.IsSuccess())
                          .Select(r => r.Match(item => item, _ => default))
                          .ToList(),
        Errors = results.Where(r => r.IsError())
                       .Select(r => r.Match(_ => default, error => error))
                       .ToList()
    };
}

JSON Serialization

Results automatically serialize to JSON with type information:

// Success result
var success = Result.Success(new User { Name = "John", Age = 30 });
var json = JsonSerializer.Serialize(success);
// {"type":"value","value":{"name":"John","age":30}}

// Error result
var error = Result.Error<User>(Error.Create().NotFound("User not found"));
var errorJson = JsonSerializer.Serialize(error);
// {"type":"error","error":{"category":{...},"reason":"User not found",...}}

// Round-trip deserialization
var deserialized = JsonSerializer.Deserialize<Result<User>>(json);

API Response Pattern

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    [HttpGet("{id}")]
    public async Task<IActionResult> GetUser(int id)
    {
        var result = await _userService.GetUserAsync(id);

        return result.Match(
            onSuccess: user => Ok(user),
            onError: error => error.Category switch
            {
                var c when c == ErrorCategory.NotFound => NotFound(error),
                var c when c == ErrorCategory.Unauthorized => Unauthorized(error),
                var c when c == ErrorCategory.ValidationFailed => BadRequest(error),
                _ => StatusCode(500, error)
            }
        );
    }

    [HttpPost]
    public async Task<IActionResult> CreateUser([FromBody] CreateUserRequest request)
    {
        var result = await _userService.CreateUserAsync(request);

        return result.Match(
            onSuccess: user => CreatedAtAction(nameof(GetUser), new { id = user.Id }, user),
            onError: error => BadRequest(error)
        );
    }
}

Extension Methods Reference

Checking

  • IsSuccess() - Returns true if result is successful
  • IsError() - Returns true if result is an error

Transformation

  • Map<TReturn>(Func<T, TReturn>) - Transform success value
  • MapAsync<TReturn>(Func<T, Task<TReturn>>) - Async transformation
  • MapError(Func<Error, Error>) - Transform error

Chaining

  • Bind<TReturn>(Func<T, Result<TReturn>>) - Chain operations
  • BindAsync<TReturn>(Func<T, Task<Result<TReturn>>>) - Async chaining

Side Effects

  • Tap(Action<T>) - Execute action on success
  • TapError(Action<Error>) - Execute action on error
  • Switch(Action<T>, Action<Error>) - Execute action based on state

Utilities

  • Match<TReturn>(Func<T, TReturn>, Func<Error, TReturn>) - Pattern matching
  • ToResult() - Convert value or error to Result
  • Flatten() - Flatten collection of Results
  • ToUntyped() - Convert Result<T> to Result

Testing

The library includes FluentAssertions extensions for testing:

using Results.Tests.Assertions;

[Fact]
public void ValidateEmail_WithValidEmail_ShouldReturnSuccess()
{
    // Arrange
    var email = "test@example.com";

    // Act
    var result = ValidateEmail(email);

    // Assert
    result.Should().BeSuccess(value => value.Should().Be(email));
}

[Fact]
public void ValidateEmail_WithInvalidEmail_ShouldReturnError()
{
    // Arrange
    var email = "invalid";

    // Act
    var result = ValidateEmail(email);

    // Assert
    result.Should().BeFailure(error =>
    {
        error.Category.Should().Be(ErrorCategory.ValidationFailed);
        error.Reason.Should().Contain("invalid");
    });
}

Best Practices

✅ DO

  • Use Result types for expected errors (validation, not found, etc.)
  • Use exceptions for unexpected errors (system failures, bugs)
  • Chain operations with Map and Bind for clean pipelines
  • Use LINQ syntax for complex multi-step operations
  • Include error context (source, category, properties)
  • Use Tap for logging and side effects
  • Create custom error types for domain-specific errors

❌ DON'T

  • Don't use Result for control flow (use if/else)
  • Don't catch and wrap all exceptions (only business logic exceptions)
  • Don't ignore errors (always handle both success and error cases)
  • Don't create Result<Result<T>> (use Bind instead)
  • Don't use Result<Error> as the generic type

Performance

The Result pattern has minimal overhead:

  • No exception throwing/catching for business logic errors
  • Struct-based error categories for zero allocation
  • Implicit conversions eliminate wrapper object creation
  • Async methods use Task efficiently

Benchmark comparison vs exceptions:

Operation Result Pattern Exception
Success path ~1 ns ~1 ns
Error path ~5 ns ~50,000 ns

Documentation

Contributing

Contributions are welcome! Please ensure:

  • All tests pass (dotnet test)
  • Code follows existing patterns
  • New features include tests
  • Documentation is updated

License

MIT License - see LICENSE file for details

Support

For issues and questions:


Built with ❤️ using functional programming principles

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.0.1 323 11/10/2025
1.0.0 158 11/9/2025

Initial release with core Result pattern implementation, functional operations (Map, Bind, LINQ), error tracking, and JSON serialization.