ResultFlow 2.2.0

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

ResultFlow

A simple, lightweight, and powerful implementation of the Result Pattern for C# that provides elegant error handling and success result management. Designed for building robust, maintainable applications with clear error flow control.

📚 Documentation Navigation

Core Packages

Package Description Documentation
ResultFlow Core Result pattern library with zero dependencies 📖 This Document
ResultFlow.AspNetCore ASP.NET Core integration with automatic HTTP status code mapping 📖 View Documentation
ResultFlow.FluentValidation FluentValidation seamless integration for validation results 📖 View Documentation

Future Extensions:

  • 🔮 ResultFlow.Logging - Structured logging integration (coming in v3.0)
  • 🔮 ResultFlow.Testing - Unit testing assertions (coming in v3.0)

✨ Features

  • Type-Safe Result Pattern - Strongly-typed success and failure handling without exceptions for flow control
  • Immutable Design - readonly struct implementation for thread-safe, memory-efficient operations
  • Functional API - Comprehensive functional programming support with Map, Bind, Filter, Match, and Tap
  • Pattern Matching - Full C# pattern matching support for elegant control flow
  • Non-Generic Result - Support for void operations that don't return values
  • Flexible Error Handling - Rich error metadata, tracing, and exception wrapping for debugging
  • Enterprise-Ready - Optimized for ASP.NET Core with seamless ActionResult conversion
  • Zero Dependencies - Core library has no external dependencies
  • MIT Licensed - Free for personal and commercial use

📦 Installation

Install the NuGet package:

dotnet add package ResultFlow

Or via Package Manager:

Install-Package ResultFlow

🚀 Quick Start

Basic Usage

using ResultFlow.Results;
using ResultFlow.Errors;

// Success case
var userResult = Result<User>.Ok(new User { Id = 1, Name = "John Doe" });

// Failure case  
var errorResult = Result<User>.Failed(
    NotFoundError.WithDefaults("User not found")
);

// Check status
if (userResult.IsOk)
{
    var user = userResult.Value;
    Console.WriteLine($"User: {user.Name}");
}

if (errorResult.HasError)
{
    Console.WriteLine($"Error: {errorResult.Error?.Message}");
}

Pattern Matching

// Match with return value
var message = userResult.Match(
    onSuccess: user => $"Welcome, {user.Name}!",
    onFailure: error => $"Error: {error.Message}"
);
Console.WriteLine(message);

// Match with side effects
userResult.Match(
    onSuccess: user => Console.WriteLine($"Found user: {user.Name}"),
    onFailure: error => Console.WriteLine($"Error: {error.Message}")
);

Void Operations

// For operations that don't return a value
public Result DeleteUser(int id)
{
    var user = _database.FindUser(id);
    if (user == null)
        return Result.Failed(NotFoundError.WithDefaults("User not found"));
    
    _database.Delete(user);
    return Result.Ok();
}

// Usage
var deleteResult = DeleteUser(1);
deleteResult.Match(
    onSuccess: () => Console.WriteLine("User deleted successfully"),
    onFailure: error => Console.WriteLine($"Error: {error.Message}")
);

📖 Core Concepts

Error Handling

ResultFlow provides multiple ways to create and handle errors:

1. PredefinedErrors - Quick Factory Methods
// Simple, quick error creation
var notFoundError = PredefinedErrors.NotFound("User");
var badRequestError = PredefinedErrors.BadRequest("Invalid input");
var conflictError = PredefinedErrors.Conflict("Email already exists");
var authError = PredefinedErrors.Unauthorized();
var validationError = PredefinedErrors.ValidationFailed("Multiple validation errors");
var serverError = PredefinedErrors.InternalError("Database connection failed");

return Result<User>.Failed(notFoundError);
2. Error Type Factory Methods - Specific Scenarios
// NotFoundError with context
var error = NotFoundError.ForResource("Product", resourceId: "PROD-123");
// Message: "The requested Product was not found."
// Metadata: { resourceName: "Product", resourceId: "PROD-123" }

// BadRequestError with field context
var error = BadRequestError.ForInvalidParameter(
    parameterName: "email",
    reason: "Invalid email format",
    providedValue: "not-an-email"
);
// Message: "The parameter 'email' is invalid: Invalid email format"
// Metadata: { parameterName: "email", providedValue: "not-an-email" }

// ConflictError with version info
var error = ConflictError.ForVersionMismatch(
    resourceName: "Document",
    expectedVersion: "v1",
    currentVersion: "v2"
);
// Message: "The Document has been modified. Expected version: v1, Current version: v2."

// ForbiddenError with role info
var error = ForbiddenError.ForMissingRole(
    requiredRole: "Admin",
    userRole: "User"
);
// Message: "This operation requires the 'Admin' role."
// Metadata: { requiredRole: "Admin", userRole: "User" }

// InternalServerError with exception
var error = InternalServerError.FromException(
    dbException,
    message: "Database query failed"
);
// Automatically includes exception type, message, and stack trace in metadata
3. ErrorBuilder - Complex Custom Errors
var error = ErrorBuilder
    .Create("PAYMENT_FAILED", "Payment processing failed")
    .WithDetails("Credit card was declined by the payment gateway")
    .AddMetadata("orderId", order.Id)
    .AddMetadata("amount", order.Total)
    .AddMetadata("paymentGateway", "Stripe")
    .AddMetadata("timestamp", DateTime.UtcNow)
    .AddMetadata("retryable", true)
    .WithException(stripeException)
    .Build();

return Result<PaymentResult>.Failed(error);

Functional Operations

Map - Transform Values
var userIdResult = Result<int>.Ok(42);

var userResult = userIdResult
    .Map(id => _userService.GetUserById(id))
    .Map(user => user.Email)
    .Map(email => new EmailDto { Address = email });

// Chain transformations with error propagation
// If any step fails, error is returned immediately
Bind - Chain Operations
public Result<User> GetUser(int id)
{
    if (id <= 0)
        return Result<User>.Failed(
            BadRequestError.ForInvalidParameter("id", "Must be positive")
        );
    
    var user = _database.FindUser(id);
    return user != null
        ? Result<User>.Ok(user)
        : Result<User>.Failed(NotFoundError.ForResource("User", id.ToString()));
}

public Result<string> GetUserEmail(int id)
{
    return GetUser(id)
        .Bind(user => ValidateUserAsync(user))
        .Bind(user => AuthorizeUserAsync(user))
        .Map(user => user.Email);
}

// Result: Ok("john@example.com") or any error from the chain
Filter - Conditional Logic
var ageResult = Result<int>.Ok(25);

var validAge = ageResult.Filter(
    age => age >= 18,
    new ValidationError("UNDERAGE", "User must be 18 or older")
);

if (validAge.IsOk)
{
    Console.WriteLine("User is old enough");
}
Tap - Side Effects
var userResult = Result<User>.Ok(new User { Id = 1, Name = "John" });

var result = userResult
    .Tap(user => Console.WriteLine($"Processing user: {user.Name}"))
    .Tap(user => _logger.LogInformation($"User ID: {user.Id}"))
    .Tap(user => _cache.Set($"user:{user.Id}", user))
    .TapError(error => _logger.LogError($"Error: {error.Message}"))
    .Map(user => user.Email);

// Tap executes side effects without changing the result
Then - Sequential Operations
var result = ValidateUser(user)
    .Then(user => SaveUserToDatabase(user))
    .Then(user => SendWelcomeEmail(user))
    .Then(user => LogUserCreation(user));

// Then chains operations sequentially
// Short-circuits on first failure

Async Operations

ResultFlow provides seamless support for async operations through Task<Result<T>> and Task<VoidResult> extensions. This allows you to chain async methods without manually awaiting them at each step.

MapAsync & BindAsync - Async Chaining
public async Task<Result<UserDto>> GetUserDtoAsync(int userId)
{
    return await _repository.GetUserAsync(userId)
        .BindAsync(user => _validator.ValidateAsync(user))
        .MapAsync(user => _mapper.MapToDtoAsync(user));
}
TapAsync - Async Side Effects
public async Task<Result<User>> CreateUserAsync(User user)
{
    return await ValidateUserAsync(user)
        .TapAsync(u => _logger.LogInformationAsync($"Creating user {u.Name}"))
        .TapAsync(u => _repository.AddAsync(u))
        .TapErrorAsync(e => _logger.LogErrorAsync($"Failed: {e.Message}"));
}
MatchAsync - Async Pattern Matching
var result = await GetUserAsync(1);

await result.MatchAsync(
    onSuccess: async user => await SendEmailAsync(user),
    onFailure: async error => await LogErrorAsync(error)
);

Error Value Retrieval

var userResult = GetUser(123);

// Option 1: Get value or default
var user = userResult.GetValueOrDefault(new User { Id = 0, Name = "Unknown" });

// Option 2: Get value or execute fallback
var user = userResult.GetValueOrElse(error => 
{
    _logger.LogError($"Failed to get user: {error.Message}");
    return new User { Id = 0, Name = "Unknown" };
});

// Option 3: Get value or throw exception
var user = userResult.GetValueOrThrow();
// Throws InvalidOperationException if failed

// Option 4: Get value with custom exception
var user = userResult.GetValueOrThrow(error => 
    new UserNotFoundException($"User not found: {error.Message}", error.InnerException)
);

🎯 Error Types

ResultFlow includes comprehensive HTTP error types with factory methods:

Error Type HTTP Code Factory Methods Use Case
BadRequestError 400 WithDefaults(), ForInvalidParameter(), ForMissingField(), ForInvalidFormat() Invalid request parameters or format
UnauthorizedError 401 WithDefaults(), ForReason() Missing or invalid authentication
ForbiddenError 403 WithDefaults(), ForMissingRole(), ForMissingPermission() User lacks required permissions
NotFoundError 404 WithDefaults(), ForResource(), ByIdentifier() Resource not found
ConflictError 409 WithDefaults(), ForDuplicateResource(), ForVersionMismatch(), ForStateConflict() Resource conflict or duplicate
ValidationError 422 WithDefaults(), ForField() Validation rule violations
InternalServerError 500 WithDefaults(), FromException(), ForOperation(), WithCode() Server-side errors

Creating Custom Errors

// Using ErrorCodes constants
var error = new Error(
    code: ErrorCodes.BadRequest.InvalidParameter,
    message: "Email parameter is invalid",
    details: "Email must follow pattern: user@example.com",
    metadata: new Dictionary<string, object>
    {
        { "fieldName", "email" },
        { "expectedFormat", "user@example.com" },
        { "providedValue", "invalid-email" }
    }
);

// Using ErrorBuilder for complex scenarios
var error = ErrorBuilder
    .Create(ErrorCodes.Custom.BusinessLogicError, "Order cannot be processed")
    .WithDetails("Customer credit limit exceeded")
    .AddMetadata("customerId", 123)
    .AddMetadata("creditLimit", 5000)
    .AddMetadata("orderTotal", 6000)
    .AddMetadata("exceededBy", 1000)
    .Build();

💡 Real-World Examples

Example 1: User Service with Validation

public class UserService
{
    private readonly IUserRepository _repository;
    private readonly ILogger<UserService> _logger;

    public async Task<Result<User>> GetUserByIdAsync(int id)
    {
        if (id <= 0)
        {
            _logger.LogWarning("Invalid user ID: {UserId}", id);
            return Result<User>.Failed(
                BadRequestError.ForInvalidParameter("id", "Must be positive", id)
            );
        }

        var user = await _repository.FindByIdAsync(id);
        if (user == null)
        {
            _logger.LogWarning("User not found: {UserId}", id);
            return Result<User>.Failed(
                NotFoundError.ByIdentifier("User", id)
            );
        }

        return Result<User>.Ok(user);
    }

    public async Task<Result<User>> CreateUserAsync(CreateUserRequest request)
    {
        // Validate input
        if (string.IsNullOrWhiteSpace(request.Email))
            return Result<User>.Failed(
                BadRequestError.ForMissingField("Email")
            );

        if (!IsValidEmail(request.Email))
            return Result<User>.Failed(
                BadRequestError.ForInvalidParameter("Email", "Invalid format", request.Email)
            );

        // Check for duplicates
        var existing = await _repository.FindByEmailAsync(request.Email);
        if (existing != null)
            return Result<User>.Failed(
                ConflictError.ForDuplicateResource("Email", request.Email)
            );

        // Create user
        try
        {
            var user = new User 
            { 
                Email = request.Email, 
                Name = request.Name,
                CreatedAt = DateTime.UtcNow
            };
            
            await _repository.AddAsync(user);
            
            _logger.LogInformation("User created: {UserId}", user.Id);
            return Result<User>.Ok(user);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error creating user");
            return Result<User>.Failed(
                InternalServerError.FromException(ex, "Error creating user")
            );
        }
    }

    public async Task<Result> DeleteUserAsync(int id)
    {
        var user = await _repository.FindByIdAsync(id);
        if (user == null)
            return Result.Failed(
                NotFoundError.ByIdentifier("User", id)
            );

        try
        {
            await _repository.DeleteAsync(user);
            _logger.LogInformation("User deleted: {UserId}", id);
            return Result.Ok();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error deleting user: {UserId}", id);
            return Result.Failed(
                InternalServerError.ForOperation("DeleteUser", ex)
            );
        }
    }

    private bool IsValidEmail(string email) 
        => email.Contains("@") && email.Contains(".");
}

Example 2: Complex Business Logic with Chaining

public async Task<Result<OrderDto>> ProcessOrderAsync(int orderId)
{
    return await GetOrderAsync(orderId)
        .Bind(order => ValidateOrderAsync(order))
        .Bind(order => CheckInventoryAsync(order))
        .Bind(order => ProcessPaymentAsync(order))
        .Bind(order => CreateShipmentAsync(order))
        .Bind(order => SendConfirmationEmailAsync(order))
        .Tap(order => _logger.LogInformation("Order processed: {OrderId}", order.Id))
        .TapError(error => _logger.LogError("Order processing failed: {Error}", error.Message))
        .Map(order => _mapper.Map<OrderDto>(order));
}

private async Task<Result<Order>> GetOrderAsync(int id)
{
    var order = await _repository.GetOrderAsync(id);
    return order != null
        ? Result<Order>.Ok(order)
        : Result<Order>.Failed(NotFoundError.ForResource("Order", id.ToString()));
}

private async Task<Result<Order>> ValidateOrderAsync(Order order)
{
    if (order.Items.Count == 0)
        return Result<Order>.Failed(
            ValidationError.WithDefaults("Order must contain at least one item")
        );

    return Result<Order>.Ok(order);
}

private async Task<Result<Order>> CheckInventoryAsync(Order order)
{
    foreach (var item in order.Items)
    {
        var stock = await _inventoryService.GetStockAsync(item.ProductId);
        if (stock < item.Quantity)
            return Result<Order>.Failed(
                ConflictError.WithDefaults($"Insufficient stock for product {item.ProductId}")
            );
    }

    return Result<Order>.Ok(order);
}

private async Task<Result<Order>> ProcessPaymentAsync(Order order)
{
    try
    {
        await _paymentService.ChargeAsync(order.CustomerId, order.Total);
        return Result<Order>.Ok(order);
    }
    catch (PaymentException ex)
    {
        return Result<Order>.Failed(
            InternalServerError.ForOperation("ProcessPayment", ex)
        );
    }
}

// ... more methods

Example 3: Validation with Filter

public Result<User> CreateUser(UserRequest request)
{
    return ValidateUserRequest(request)
        .Filter(
            user => user.Age >= 18,
            ValidationError.WithDefaults("User must be 18 years old")
        )
        .Filter(
            user => !user.Email.Contains("invalid"),
            BadRequestError.ForInvalidParameter("Email", "Invalid email domain")
        )
        .Tap(user => _repository.Add(user))
        .Tap(user => _logger.LogInformation("User created: {UserId}", user.Id));
}

private Result<User> ValidateUserRequest(UserRequest request)
{
    if (string.IsNullOrWhiteSpace(request.Name))
        return Result<User>.Failed(
            BadRequestError.ForMissingField("Name")
        );

    if (string.IsNullOrWhiteSpace(request.Email))
        return Result<User>.Failed(
            BadRequestError.ForMissingField("Email")
        );

    return Result<User>.Ok(new User 
    { 
        Name = request.Name, 
        Email = request.Email,
        Age = request.Age
    });
}

📚 API Reference

Result<TValue>

// Factory Methods
Result<T> Ok(T value)              // Create success result
Result<T> Failed(Error error)          // Create failure result

// Properties
T? Value                                // Success value
Error? Error                            // Error information
bool IsOk                              // Check if successful
bool HasError                          // Check if failed

// Functional Methods
Result<TResult> Map<TResult>(Func<T, TResult> map)
Result<TResult> Bind<TResult>(Func<T, Result<TResult>> bind)
Result<TResult> Filter(Func<T, bool> predicate, Error error)
Result<T> Tap(Action<T> action)
Result<T> TapError(Action<Error> action)
Result<TResult> Then<TResult>(Result<TResult> other)
Result<TResult> Then<TResult>(Func<T, Result<TResult>> then)

// Pattern Matching
TResult Match<TResult>(Func<T, TResult> onSuccess, Func<Error, TResult> onFailure)
void Match(Action<T> onSuccess, Action<Error> onFailure)

// Value Retrieval
T? GetValueOrDefault(T? defaultValue = default)
T? GetValueOrElse(Func<Error, T?> onFailure)
T? GetValueOrThrow()
T? GetValueOrThrow(Func<Error, Exception> exceptionFactory)

// Equality
bool Equals(Result<T> other)
override int GetHashCode()
override string ToString()

Result (Void)

// Factory Methods
Result Ok()                        // Create success result
Result Failed(Error error)             // Create failure result

// Properties
Error? Error                            // Error information
bool IsOk                              // Check if successful
bool HasError                          // Check if failed

// Functional Methods
Result Tap(Action action)
Result TapError(Action<Error> action)

// Pattern Matching
void Match(Action onSuccess, Action<Error> onFailure)
TResult Match<TResult>(Func<TResult> onSuccess, Func<Error, TResult> onFailure)

// Utility
Error? GetErrorOrThrow()
override string ToString()

Error

// Properties
string Code                             // Error code identifier
string Message                          // Human-readable message
string? Details                         // Additional details
Dictionary<string, object>? Metadata    // Custom metadata
Exception? InnerException               // Underlying exception

ErrorBuilder

ErrorBuilder WithCode(string code)
ErrorBuilder WithMessage(string message)
ErrorBuilder WithDetails(string details)
ErrorBuilder WithMetadata(Dictionary<string, object> metadata)
ErrorBuilder AddMetadata(string key, object value)
ErrorBuilder AddMetadataRange(Dictionary<string, object> metadata)
ErrorBuilder WithException(Exception exception)
ErrorBuilder ClearMetadata()
Error Build()
TError Build<TError>() where TError : Error, new()

static ErrorBuilder Create(string code, string message)
static ErrorBuilder Empty()

Task Extensions

Extensions for Task<Result<T>> and Task<VoidResult>:

// Async Transformations
Task<Result<TNew>> MapAsync<T, TNew>(this Task<Result<T>>, Func<T, Task<TNew>>)
Task<Result<TNew>> BindAsync<T, TNew>(this Task<Result<T>>, Func<T, Task<Result<TNew>>>)

// Async Side Effects
Task<Result<T>> TapAsync<T>(this Task<Result<T>>, Func<T, Task>)
Task<Result<T>> TapErrorAsync<T>(this Task<Result<T>>, Func<Error, Task>)

// Async Pattern Matching
Task<TResult> MatchAsync<T, TResult>(this Task<Result<T>>, Func<T, Task<TResult>>, Func<Error, Task<TResult>>)

🤝 Contributing

Contributions are welcome! Please feel free to submit issues or pull requests on GitHub.

Development Setup

git clone https://github.com/saidshl/ResultFlow.git
cd ResultFlow
dotnet build
dotnet test

💬 Support

For questions, issues, or suggestions:


Made with ❤️ by Said Souhayel

Last updated: 2025-11-23 10:13:05 UTC

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 is compatible.  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 is compatible.  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.
  • net10.0

    • No dependencies.
  • net8.0

    • No dependencies.
  • net9.0

    • No dependencies.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on ResultFlow:

Package Downloads
ResultFlow.FluentValidation

FluentValidation integration for ResultFlow. Provides seamless conversion between FluentValidation ValidationResult and the Result pattern, enabling fluent validation with railway-oriented programming.

ResultFlow.AspNetCore

ASP.NET Core integration for ResultFlow. Provides extension methods to convert Result and Result<T> to IActionResult with automatic HTTP status code mapping based on error types.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.2.0 490 12/8/2025
2.0.2 238 11/25/2025
2.0.0 234 11/25/2025