FunctionalDdd.RailwayOrientedProgramming 2.1.10

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

Railway Oriented Programming

NuGet Package

Railway Oriented Programming (ROP) is a functional approach to error handling that treats your code like a railway track. Operations either succeed (staying on the success track) or fail (switching to the error track). This library provides the core types and extension methods to implement ROP in C#.

Table of Contents

Installation

Install via NuGet:

dotnet add package FunctionalDDD.RailwayOrientedProgramming

Core Concepts

Result Type

The Result<TValue> type represents either a successful computation (with a value) or a failure (with an error).

public readonly struct Result<TValue>
{
    public TValue Value { get; }        // Throws if IsFailure
    public Error Error { get; }         // Throws if IsSuccess
    
    public bool IsSuccess { get; }
    public bool IsFailure { get; }

    // Implicit conversions
    public static implicit operator Result<TValue>(TValue value);
    public static implicit operator Result<TValue>(Error error);
}

Basic Usage:

using FunctionalDdd;

// Success result
Result<int> success = Result.Success(42);
Result<int> alsoSuccess = 42; // Implicit conversion

// Failure result
Result<int> failure = Result.Failure<int>(Error.NotFound("Item not found"));
Result<int> alsoFailure = Error.NotFound("Item not found"); // Implicit conversion

// Checking state
if (success.IsSuccess)
{
    var value = success.Value; // 42
}

if (failure.IsFailure)
{
    var error = failure.Error; // Error object
}

Maybe Type

The Maybe<T> type represents an optional value that may or may not exist.

public readonly struct Maybe<T> : IEquatable<T>, IEquatable<Maybe<T>>
    where T : notnull
{
    public T Value { get; }
    public bool HasValue { get; }
    public bool HasNoValue { get; }
}

Basic Usage:

// Create Maybe with value
Maybe<string> some = Maybe.From("hello");
Maybe<string> alsoSome = "hello"; // Implicit conversion

// Create Maybe without value
Maybe<string> none = Maybe.None<string>();
Maybe<string> alsoNone = null; // For reference types

// Check and use
if (some.HasValue)
{
    Console.WriteLine(some.Value); // "hello"
}

// Get value or default
string result = none.GetValueOrDefault("default"); // "default"

Error Types

The library provides several built-in error types:

  • Error - Base error class
  • NotFoundError - Resource not found
  • ValidationError - Input validation failure
  • ConflictError - Business rule conflict
  • UnauthorizedError - Authentication required
  • ForbiddenError - Insufficient permissions
  • UnexpectedError - Unexpected system error
  • AggregatedError - Multiple errors combined
var notFound = Error.NotFound("User not found", "userId");
var validation = Error.Validation("Email is invalid", "email");
var conflict = Error.Conflict("Email already exists");
var unauthorized = Error.Unauthorized("Login required");

Getting Started

Here's a simple example demonstrating the power of Railway Oriented Programming:

public record User(string Id, string Email, bool IsActive);

public Result<User> GetActiveUser(string userId)
{
    return GetUserById(userId)
        .ToResult(Error.NotFound($"User {userId} not found"))
        .Ensure(user => user.IsActive, 
               Error.Validation("User account is not active"))
        .Tap(user => LogUserAccess(user.Id));
}

private User? GetUserById(string id) { /* ... */ }
private void LogUserAccess(string userId) { /* ... */ }

Core Operations

Bind

Bind chains operations that return Result. It calls the function only if the current result is successful.

Use when: You need to chain operations where each step can fail.

// Basic bind
Result<int> ParseAge(string input) => 
    int.TryParse(input, out var age) 
        ? Result.Success(age) 
        : Error.Validation("Invalid age");

Result<string> ValidateAge(int age) =>
    age >= 18 
        ? Result.Success($"Age {age} is valid") 
        : Error.Validation("Must be 18 or older");

var result = ParseAge("25")
    .Bind(age => ValidateAge(age)); // Success("Age 25 is valid")

var invalid = ParseAge("15")
    .Bind(age => ValidateAge(age)); // Failure

Async variant:

async Task<Result<User>> GetUserAsync(string id) { /* ... */ }
async Task<Result<Order>> GetLastOrderAsync(User user) { /* ... */ }

var result = await GetUserAsync("123")
    .BindAsync(user => GetLastOrderAsync(user));

Map

Map transforms the value inside a successful Result. Unlike Bind, the transformation function returns a plain value, not a Result.

Use when: You need to transform a value without introducing failure.

var result = Result.Success(5)
    .Map(x => x * 2)           // Success(10)
    .Map(x => x.ToString());   // Success("10")

// With failure
var failure = Result.Failure<int>(Error.NotFound("Number not found"))
    .Map(x => x * 2);          // Still Failure, Map is not called

Async variant:

var result = await GetUserAsync("123")
    .MapAsync(user => user.Email.ToLowerInvariant());

Tap

Tap executes a side effect (like logging) on success without changing the result. It returns the same Result.

Use when: You need to perform side effects (logging, metrics, etc.) without transforming the value.

var result = Result.Success(42)
    .Tap(x => Console.WriteLine($"Value: {x}"))  // Logs "Value: 42"
    .Tap(x => _metrics.IncrementCounter())       // Records metric
    .Map(x => x * 2);                            // Success(84)

// With failure - Tap is skipped
var failure = Result.Failure<int>(Error.NotFound("Not found"))
    .Tap(x => Console.WriteLine("This won't run"))
    .Map(x => x * 2);  // Still Failure

Async variant:

var result = await GetUserAsync("123")
    .TapAsync(async user => await AuditLogAsync(user.Id))
    .TapAsync(user => SendWelcomeEmail(user.Email));

TapError:

var result = GetUser("123")
    .TapError(error => _logger.LogError($"Failed: {error.Message}"))
    .Tap(user => _logger.LogInfo($"Success: {user.Name}"));

Ensure

Ensure validates a condition on success. If the condition is false, it returns a failure with the specified error.

Use when: You need to validate business rules or conditions.

Result<User> CreatePremiumUser(string name, int age)
{
    return User.Create(name, age)
        .Ensure(user => user.Age >= 18, 
               Error.Validation("Must be 18 or older"))
        .Ensure(user => !string.IsNullOrEmpty(user.Name), 
               Error.Validation("Name is required"))
        .Tap(user => user.GrantPremiumAccess());
}

Multiple conditions:

var result = GetProduct(productId)
    .Ensure(p => p.Stock > 0, Error.Validation("Out of stock"))
    .Ensure(p => p.Price > 0, Error.Validation("Invalid price"))
    .Ensure(p => !p.IsDiscontinued, Error.Validation("Product discontinued"));

Async variant:

var result = await GetUserAsync("123")
    .EnsureAsync(async user => await IsEmailVerifiedAsync(user.Email),
                Error.Validation("Email not verified"));

Compensate

Compensate provides error recovery by calling a fallback function when a result fails. Useful for providing default values or alternative paths.

Use when: You need fallback behavior or error recovery.

Basic compensation:

// Compensate without accessing the error
Result<User> result = GetUser(userId)
    .Compensate(() => CreateGuestUser());

// Compensate with access to the error
Result<User> result = GetUser(userId)
    .Compensate(error => CreateUserFromError(error));

Conditional compensation with predicate:

Compensate only when specific error conditions are met:

// Compensate only for NotFound errors
Result<User> result = GetUser(userId)
    .Compensate(
        predicate: error => error is NotFoundError,
        func: () => CreateDefaultUser()
    );

// Compensate with error context
Result<User> result = GetUser(userId)
    .Compensate(
        predicate: error => error is NotFoundError,
        func: error => CreateUserFromError(error)
    );

// Compensate based on error code
Result<Data> result = FetchData(id)
    .Compensate(
        predicate: error => error.Code == "not.found.error",
        func: () => GetCachedData(id)
    );

// Compensate for multiple error types
Result<Config> result = LoadConfig()
    .Compensate(
        predicate: error => error is NotFoundError or UnauthorizedError,
        func: () => GetDefaultConfig()
    );

Async variant:

var result = await GetUserAsync(userId)
    .CompensateAsync(async error => await GetFromCacheAsync(userId));

Combine

Combine aggregates multiple Result objects. If all succeed, returns success with all values. If any fail, returns all errors combined.

Use when: You need to validate multiple independent operations before proceeding.

// Combine multiple validations
var result = EmailAddress.TryCreate("user@example.com")
    .Combine(FirstName.TryCreate("John"))
    .Combine(LastName.TryCreate("Doe"))
    .Bind((email, firstName, lastName) => 
        User.Create(email, firstName, lastName));

// All validations must pass
if (result.IsSuccess)
{
    var user = result.Value; // All inputs were valid
}
else
{
    var errors = result.Error; // Contains all validation errors
}

With optional values:

In this scenario, firstName is optional. If provided, it will be validated; if not, it will be skipped. In other words, FirstName.TryCreate is only called if firstName is not null.

string? firstName = null;  // Optional
string email = "user@example.com";
string? lastName = "Doe";

var result = EmailAddress.TryCreate(email)
    .Combine(Maybe.Optional(firstName, FirstName.TryCreate))
    .Combine(Maybe.Optional(lastName, LastName.TryCreate))
    .Bind((e, f, l) => CreateProfile(e, f, l));

Finally

Finally unwraps a Result by providing handlers for both success and failure cases. It always returns a value (not a Result).

Use when: You need to convert a Result to a concrete value or action.

// Convert to string message
string message = GetUser("123")
    .Finally(
        ok: user => $"Found: {user.Name}",
        err: error => $"Error: {error.Message}"
    );

// Convert to HTTP status
int statusCode = SaveData(data)
    .Finally(
        ok: _ => 200,
        err: error => error switch
        {
            NotFoundError => 404,
            ValidationError => 400,
            _ => 500
        }
    );

Async variant:

var response = await ProcessOrderAsync(order)
    .FinallyAsync(
        ok: async order => await CreateSuccessResponseAsync(order),
        err: async error => await CreateErrorResponseAsync(error)
    );

Advanced Features

LINQ Query Syntax

You can use C# query expressions with Result via Select, SelectMany, and Where:

// Chaining operations with query syntax
var total = from a in Result.Success(2)
            from b in Result.Success(3)
            from c in Result.Success(5)
            select a + b + c;  // Success(10)

// With failure
var result = from x in Result.Success(5)
             where x > 10  // Predicate fails -> UnexpectedError
             select x;

// Practical example
var userOrder = from user in GetUser(userId)
                from order in GetOrder(orderId)
                where order.UserId == user.Id
                select (user, order);

Note: where uses an UnexpectedError if the predicate fails. For domain-specific errors, prefer Ensure.

Pattern Matching

Use Match to handle both success and failure cases inline:

// Synchronous match
var description = GetUser("123").Match(
    ok: user => $"User: {user.Name}",
    err: error => $"Error: {error.Code}"
);

// Async match
await ProcessOrderAsync(order).MatchAsync(
    ok: async order => await SendConfirmationAsync(order),
    err: async error => await LogErrorAsync(error)
);

// With return value
var httpResult = SaveData(data).Match(
    ok: data => Results.Ok(data),
    err: error => error.ToErrorResult()
);

Exception Capture

Use Try and TryAsync to safely capture exceptions and convert them to Result:

Use when: Integrating with code that throws exceptions.

// Synchronous
Result<string> LoadFile(string path)
{
    return Result.Try(() => File.ReadAllText(path));
}

// Async
async Task<Result<User>> FetchUserAsync(string url)
{
    return await Result.TryAsync(async () => 
        await _httpClient.GetFromJsonAsync<User>(url));
}

// Usage
var content = LoadFile("config.json")
    .Ensure(c => !string.IsNullOrEmpty(c), 
           Error.Validation("File is empty"))
    .Bind(ParseConfig);

Parallel Operations

Run multiple async operations in parallel and combine their results:

var result = await GetStudentInfoAsync(studentId)
    .ParallelAsync(GetStudentGradesAsync(studentId))
    .ParallelAsync(GetLibraryBooksAsync(studentId))
    .AwaitAsync()
    .BindAsync((info, grades, books) => 
        PrepareReport(info, grades, books));

Error Transformation

Transform errors while preserving success values:

Result<int> GetUserPoints(string userId) { /* ... */ }

var apiResult = GetUserPoints(userId)
    .MapError(err => Error.NotFound($"Points for user {userId} not found"));

// Success values pass through unchanged
// Failure errors are replaced with the new error

Common Patterns

Validation Pipeline

public Result<Order> ProcessOrder(OrderRequest request)
{
    return ValidateRequest(request)
        .Bind(req => CheckInventory(req.ProductId, req.Quantity))
        .Bind(product => ValidatePayment(request.PaymentInfo))
        .Bind(payment => CreateOrder(request, payment))
        .Tap(order => SendConfirmationEmail(order))
        .TapError(error => LogOrderFailure(error));
}

Error Recovery with Fallbacks

public Result<Config> LoadConfiguration()
{
    return LoadFromFile("config.json")
        .Compensate(error => error is NotFoundError, 
                   () => LoadFromEnvironment())
        .Compensate(error => error is NotFoundError, 
                   () => GetDefaultConfig())
        .Ensure(cfg => cfg.IsValid, 
               Error.Validation("Invalid configuration"));
}

Multi-Field Validation

public Result<User> RegisterUser(string email, string firstName, string lastName, int age)
{
    return EmailAddress.TryCreate(email)
        .Combine(FirstName.TryCreate(firstName))
        .Combine(LastName.TryCreate(lastName))
        .Combine(EnsureExtensions.Ensure(age >= 18, 
                Error.Validation("Must be 18 or older", "age")))
        .Bind((e, f, l) => User.Create(e, f, l, age));
}

Async Chain with Side Effects

public async Task<Result<string>> PromoteCustomerAsync(string customerId)
{
    return await GetCustomerByIdAsync(customerId)
        .ToResultAsync(Error.NotFound($"Customer {customerId} not found"))
        .EnsureAsync(customer => customer.CanBePromoted,
                    Error.Validation("Customer has highest status"))
        .TapAsync(customer => customer.PromoteAsync())
        .BindAsync(customer => SendPromotionEmailAsync(customer.Email))
        .FinallyAsync(
            ok: _ => "Promotion successful",
            err: error => error.Message
        );
}

Best Practices

  1. Use Bind for operations that can fail, Map for pure transformations

    // Good
    GetUser(id)
        .Map(user => user.Name)           // Pure transformation
        .Bind(name => ValidateName(name)) // Can fail
    
    // Avoid
    GetUser(id)
        .Bind(user => Result.Success(user.Name)) // Unnecessary Result wrapping
    
  2. Prefer Ensure over Bind for simple validations

    // Good
    GetUser(id)
        .Ensure(user => user.IsActive, Error.Validation("User not active"))
    
    // Avoid
    GetUser(id)
        .Bind(user => user.IsActive 
            ? Result.Success(user) 
            : Error.Validation("User not active"))
    
  3. Use Tap for side effects (logging, metrics, notifications)

    ProcessOrder(order)
        .Tap(o => _logger.LogInfo($"Order {o.Id} processed"))
        .Tap(o => _metrics.RecordOrder(o))
        .TapError(err => _logger.LogError(err.Message))
    
  4. Combine independent validations instead of nesting

    // Good
    Email.TryCreate(email)
        .Combine(Name.TryCreate(name))
        .Combine(Age.TryCreate(age))
        .Bind((e, n, a) => User.Create(e, n, a))
    
    // Avoid
    Email.TryCreate(email)
        .Bind(e => Name.TryCreate(name)
            .Bind(n => Age.TryCreate(age)
                .Bind(a => User.Create(e, n, a))))
    
  5. Use domain-specific errors instead of generic ones

    // Good
    Error.Validation("Email format is invalid", "email")
    
    // Avoid
    Error.Unexpected("Something went wrong")
    
  6. Handle errors at boundaries (controllers, entry points)

    [HttpPost]
    public ActionResult<User> Register(RegisterRequest request) =>
        RegisterUser(request)
            .ToActionResult(this);  // Converts Result to ActionResult
    
  7. Use Try/TryAsync for exception boundaries

    Result<Data> LoadData() =>
        Result.Try(() => File.ReadAllText(path))
            .Bind(json => ParseJson(json));
    
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.

NuGet packages (3)

Showing the top 3 NuGet packages that depend on FunctionalDdd.RailwayOrientedProgramming:

Package Downloads
FunctionalDdd.FluentValidation

Convert fluent validation errors to FunctionalDdd Validation errors.

FunctionalDdd.Asp

These extension methods are used to convert the ROP Result object to ActionResult. If the Result is in a failed state, it returns the corresponding HTTP error code.

FunctionalDdd.CommonValueObjects

To avoid passing around strings, it is recommended to use RequiredString to obtain strongly typed properties. The source code generator will automate the implementation process.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.1.10 703 12/3/2025
2.1.9 306 11/21/2025
2.1.1 261 4/26/2025
2.1.0-preview.3 90 4/26/2025
2.0.1 259 1/23/2025
2.0.0-alpha.62 80 1/8/2025
2.0.0-alpha.61 79 1/7/2025
2.0.0-alpha.60 95 12/7/2024
2.0.0-alpha.55 88 11/22/2024
2.0.0-alpha.52 97 11/7/2024
2.0.0-alpha.48 85 11/2/2024
2.0.0-alpha.47 86 10/30/2024
2.0.0-alpha.44 151 10/18/2024
2.0.0-alpha.42 106 10/14/2024
2.0.0-alpha.39 125 6/27/2024
2.0.0-alpha.38 107 4/24/2024
2.0.0-alpha.33 100 4/17/2024
2.0.0-alpha.26 123 4/9/2024
2.0.0-alpha.21 111 4/1/2024
2.0.0-alpha.19 98 3/5/2024
2.0.0-alpha.18 100 2/28/2024
2.0.0-alpha.17 104 2/26/2024
2.0.0-alpha.15 112 1/30/2024
2.0.0-alpha.8 98 1/27/2024
2.0.0-alpha.6 131 1/5/2024
1.1.1 1,108 11/15/2023
1.1.0-alpha.32 152 11/2/2023
1.1.0-alpha.30 243 11/1/2023
1.1.0-alpha.28 128 10/28/2023
1.1.0-alpha.27 127 10/28/2023
1.1.0-alpha.24 116 10/20/2023
1.1.0-alpha.23 125 10/13/2023
1.1.0-alpha.21 151 10/1/2023
1.1.0-alpha.20 126 9/30/2023
1.1.0-alpha.19 153 9/30/2023
1.1.0-alpha.18 132 9/29/2023
1.1.0-alpha.17 119 9/22/2023
1.1.0-alpha.13 103 9/16/2023
1.1.0-alpha.4 227 6/9/2023
1.1.0-alpha.3 166 6/8/2023
1.0.1 1,348 5/12/2023
0.1.0-alpha.40 217 4/6/2023
0.1.0-alpha.39 215 4/3/2023
0.1.0-alpha.38 246 4/2/2023
0.1.0-alpha.37 217 3/31/2023
0.1.0-alpha.35 222 3/29/2023
0.1.0-alpha.34 198 3/28/2023
0.1.0-alpha.32 236 3/18/2023
0.1.0-alpha.30 219 3/11/2023
0.1.0-alpha.27 214 3/7/2023
0.1.0-alpha.24 226 2/15/2023
0.1.0-alpha.22 222 2/15/2023
0.1.0-alpha.20 231 2/13/2023
0.0.1-alpha.14 230 1/4/2023
0.0.1-alpha.4 222 12/30/2022