Sviatoslav93.FunctionalPrimitives 1.0.1

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

Functional Primitives for C#

Build Status NuGet License: MIT

A robust, functional approach to error handling in C#. The Result pattern is an alternative to exception-based error handling that makes error states explicit and composable. The Maybe monad is a powerful tool for handling errors, allowing for more expressive and composable code.

Features

  • ๐Ÿš€ Zero exceptions - Explicit error handling without throwing exceptions
  • ๐Ÿ”— Fluent chaining - Chain operations with Bind, BindAsync, and more
  • ๐ŸŽฏ Type safe - Compile-time safety for error handling
  • โšก High performance - No exception overhead
  • ๐Ÿงช Composable - Combine, validate, and transform results easily
  • ๐Ÿ”„ Async support - Full async/await integration
  • ๐Ÿ“ฆ Lightweight - Minimal dependencies

Installation

dotnet add package Sviatoslav93.FunctionalPrimitives

Quick Start

using Result;
using Result.Extensions;

// Basic usage
Result<int> Divide(int a, int b)
{
    if (b == 0)
    {
        return new Error("Division by zero");
    }

    return a / b;
}

// Chain operations
var result = Divide(10, 2)
    .Bind(x => x * 2)
    .Bind(x => x.ToString())
    .Match(
        onSuccess: value => $"Result: {value}",
        onFailure: errors => $"Error: {string.Join(", ", errors)}");

Console.WriteLine(result); // Output: Result: 10

Core Concepts

Result<T>

The Result<T> type represents either a successful value of type T or a collection of errors:

// Success
Result<string> success = Result.Success("Hello World");
Result<string> implicit = "Hello World"; // Implicit conversion

// Failure
Result<string> failure = Result.Failure<string>(new Error("Something went wrong"));
Result<string> implicitError = new Error("Something went wrong"); // Implicit conversion

Error

The Error record represents an error with contextual information:

// Simple error
var error = new Error("Invalid input");

// Error with code
var codedError = new Error("Invalid email format", "EMAIL_INVALID");

// Error with exception can be created in the Try method
var result = Result.Try(() => int.Parse("invalid"), ex => new Error(ex.Message));

Usage Examples

Basic Operations

// Creating results
Result<int> success = 42;
Result<int> failure = new Error("Invalid number");

// Checking success/failure
if (result.IsSuccess)
{
    Console.WriteLine($"Value: {result.Value}");
}
else
{
    Console.WriteLine($"Errors: {string.Join(", ", result.Errors)}");
}

// Pattern matching
var message = result.Match(
    onSuccess: value => $"Got: {value}",
    onFailure: errors => $"Failed: {string.Join(", ", errors)}");

Chaining Operations

// Synchronous chaining
var result = ParseInt("123")
    .Bind(x => x * 2)
    .Bind(x => x > 100 ? x : new Error("Value too small"))
    .Bind(x => x.ToString());

// Async chaining
var asyncResult = await GetUserAsync(userId)
    .BindAsync(user => GetProfileAsync(user.Id))
    .BindAsync(profile => UpdateLastAccessAsync(profile))
    .MatchAsync(
        onSuccess: profile => $"Updated: {profile.Name}",
        onFailure: errors => $"Failed: {string.Join(", ", errors)}");

Error Handling and Validation

// Validation pipeline
Result<string> ValidateEmail(string email)
{
    if (string.IsNullOrWhiteSpace(email))
        return new Error("Email is required");

    if (!email.Contains("@"))
        return new Error("Invalid email format");

    if (email.Length > 100)
        return new Error("Email too long");

    return email;
}

// Chain validations
var userResult = ValidateName(name)
    .Bind(n => ValidateEmail(email)
        .Bind(e => ValidateAge(age)
            .Bind(a => new User(n, e, a))));

Combining Results

// Combine multiple results
var nameResult = GetName();
var emailResult = GetEmail();
var ageResult = GetAge();

var userResult = nameResult.Combine(emailResult, ageResult)
    .Bind(values => new User(values[0], values[1], int.Parse(values[2])));

// All must succeed, or you get all errors
if (!userResult.IsSuccess)
{
    foreach (var error in userResult.Errors)
    {
        Console.WriteLine($"Validation error: {error.Message}");
    }
}

Exception Safety

// Safely execute code that might throw
var result = Result.Try(() =>
{
    return int.Parse("not-a-number"); // This will throw
});

// result.IsSuccess == false
// result.Errors contains the exception details

// Custom error handling
var customResult = Result.Try(
    () => RiskyOperation(),
    ex => new Error($"Operation failed: {ex.Message}", "RISKY_OP_FAILED"));

Working with Unit Type

For operations that don't return values:

// Use Unit for void operations
Result<Unit> SaveData(string data)
{
    try
    {
        File.WriteAllText("data.txt", data);
        return Unit.Value;
    }
    catch (Exception ex)
    {
        return new Error("Failed to save data");
    }
}

// Chain void operations
var saveResult = await ValidateData(data)
    .BindAsync(d => SaveDataAsync(d))
    .BindAsync(_ => LogSuccessAsync())
    .BindAsync(_ => NotifyUsersAsync());

Advanced Patterns

Recovery and Fallbacks
// Provide fallback values
var result = GetPrimaryData()
    .Recover("default-value")
    .Recover(errors => $"Fallback for: {errors.Count()} errors");

// Conditional recovery
var recovered = GetUserPreferences()
    .Recover(errors =>
        errors.Any(e => e.Code == "NOT_FOUND")
            ? "default-preferences"
            : throw new InvalidOperationException());
Side Effects with Tap
// Perform side effects without changing the result
var result = await ProcessDataAsync(input)
    .TapAsync(data => LogProcessedAsync(data))
    .TapAsync(data => CacheResultAsync(data))
    .TapErrorAsync(errors => LogErrorsAsync(errors))
    .BindAsync(data => TransformAsync(data));

Validation Guards with Ensure

// Validate a value before wrapping in a result
var result = userInput
    .Ensure(s => !string.IsNullOrWhiteSpace(s), new Error("Input is required"))
    .Ensure(s => s.Length <= 100, new Error("Input is too long"));

// Validate within a result chain
var result = ParseInt(input)
    .Ensure(n => n > 0, new Error("Value must be positive"))
    .Ensure(n => n < 1000, new Error("Value must be less than 1000"));

// Async validation
var result = await GetUserAsync(id)
    .EnsureAsync(user => IsActiveAsync(user), new Error("User is not active"));

Maybe<T> โ€” Optional Values

Maybe<T> represents an optional value that is either present (Some) or absent (None). It replaces nullable references and null checks with a type-safe, composable alternative.

using FunctionalPrimitives;
using FunctionalPrimitives.Extensions.Maybe;

// Creating Maybe values
Maybe<string> some = Maybe.Some("hello");
Maybe<string> none = Maybe.None<string>();

// From nullable
string? nullable = GetNullableString();
Maybe<string> maybe = nullable.ToMaybe();      // class
int? nullableInt = GetNullableInt();
Maybe<int> maybeInt = nullableInt.ToMaybe();   // struct

// Checking presence
if (maybe.HasValue)
{
    Console.WriteLine(maybe.Value);
}

// Safe fallback
string value = maybe.GetValueOrDefault("default");

// GetValueOr (extension)
string value2 = maybe.GetValueOr("fallback");

// Pattern matching
var message = maybe.Match(
    some: v => $"Found: {v}",
    none: () => "Nothing here");

// Convert to Result
Result<string> result = maybe.ToResult(new Error("Value not found"));

// Chaining
var result2 = GetOptionalUser(id)
    .ToMaybe()
    .Bind(user => GetOptionalProfile(user.Id))
    .ToResult(new Error("User or profile not found"));

JSON Serialization

Result<T> has built-in JSON support via System.Text.Json. The converter is applied automatically through the [JsonConverter] attribute on Result<T>.

var result = Result.Success(new User("Alice", 30));

// Serialize
string json = result.ToJson();
// {"isSuccess":true,"value":{"name":"Alice","age":30}}

Result<User> failure = new Error("Not found", "USER_NOT_FOUND");
string failureJson = failure.ToJson();
// {"isSuccess":false,"errors":[{"message":"Not found","code":"USER_NOT_FOUND","type":"","metadata":null}]}

// Works with JsonSerializer directly
string json2 = JsonSerializer.Serialize(result);

// Works in ASP.NET Core responses โ€” no extra configuration needed
app.MapGet("/user/{id}", (int id) => GetUser(id)); // Result<User> returned directly

Task<Result<T>> Integration

// Working with Task<Result<T>>
Task<Result<User>> GetUserAsync(int id);

var result = await GetUserAsync(123)
    .BindAsync(user => GetProfileAsync(user.Id))
    .BindAsync(profile => EnrichProfileAsync(profile))
    .MatchAsync(
        onSuccess: profile => $"Welcome {profile.DisplayName}",
        onFailure: errors => "Failed to load profile");

Best Practices

1. Use Implicit Conversions

// Good: Clean and readable
Result<string> GetUserName() => "John Doe";
Result<string> GetError() => new Error("User not found");

// Avoid: Verbose
Result<string> GetUserNameVerbose() => Result.Success("John Doe");

2. Chain Operations for Readability

// Good: Fluent pipeline
var result = input
    .ToResult()
    .Bind(Validate)
    .Bind(Transform)
    .Bind(Save)
    .Match(
        onSuccess: data => $"Saved: {data.Id}",
        onFailure: errors => LogAndReturnError(errors));

3. Use Meaningful Error Messages and Codes

// Good: Descriptive errors with codes
if (age < 0)
    return new Error("Age cannot be negative", "INVALID_AGE");

// Avoid: Generic errors
if (age < 0)
    return new Error("Invalid input");

4. Combine Multiple Results

// Combine multiple results into one
var nameResult = GetName();
var emailResult = GetEmail();
var ageResult = GetAge();

var userResult = nameResult.Combine(emailResult, ageResult)
    .Bind(values => new User(values[0], values[1], int.Parse(values[2])));

Performance

The Result pattern eliminates exception overhead:

// Traditional exception-based (slow for expected failures)
try
{
    var result = int.Parse(userInput);
    return result * 2;
}
catch (FormatException)
{
    return -1; // Default value
}

// Result-based (fast)
return Result.Try(() => int.Parse(userInput))
    .Recover(-1)
    .Match(
        x => x * 2,
        _ => -1);

Testing

Results make testing error conditions straightforward:

[Fact]
public void Should_Return_Error_When_Division_By_Zero()
{
    var result = Calculator.Divide(10, 0);

    Assert.False(result.IsSuccess);
    Assert.Single(result.Errors);
    Assert.Contains("division by zero", result.Errors[0].Message);
}

[Fact]
public void Should_Chain_Successful_Operations()
{
    var result = Calculator.Divide(10, 2)
        .Bind(x => x * 3)
        .Bind(x => x.ToString());

    Assert.True(result.IsSuccess);
    Assert.Equal("15", result.Value);
}

License

This project is licensed under the MIT License - see the LICENSE file for details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

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 239 3/28/2026
1.0.0 120 3/27/2026