UnambitiousFx.Functional 2.0.0

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

UnambitiousFx.Functional

Build Status NuGet NuGet Downloads codecov License: MIT .NET

A lightweight, modern functional programming library for .NET that makes failure handling and optional values elegant and type-safe.

๐Ÿ”ง Compatibility & support

  • Dependency-free: No external runtime dependencies โ€” the library is dependency-less at runtime.
  • Supported .NET versions: Supports all Microsoft LTS releases and the latest non-LTS release. See the CI matrix or the Releases page for exact versions.

๐ŸŽฏ Features

  • Result<T> - Railway-oriented programming for failure handling without exceptions
  • Maybe<T> - Type-safe optional values, no more null reference exceptions
  • OneOf<T1..T10> - Discriminated unions via source generators
  • Rich Failure Types - ValidationFailure, NotFoundFailure, ConflictFailure, UnauthorizedFailure, and more
  • Comprehensive Extensions - Bind, Map, Match, Tap, Ensure, Recover, and dozens more
  • Async First - Full support for Task<T> and ValueTask<T>
  • ASP.NET Core Integration - Convert results to HTTP responses effortlessly
  • Metadata Support - Attach contextual information to results
  • xUnit Testing Utilities - Fluent assertions for functional types
  • Performance Focused - Zero allocation where possible, benchmarks included
  • Debugger Friendly - Enhanced debugging experience with custom visualizers

๐Ÿ“ฆ Installation

dotnet add package UnambitiousFx.Functional

For ASP.NET Core integration:

dotnet add package UnambitiousFx.Functional.AspNetCore

For xUnit testing utilities:

dotnet add package UnambitiousFx.Functional.xunit

๐Ÿš€ Quick Start

Result<T> - Failure Handling Made Simple

Instead of throwing exceptions:

using UnambitiousFx.Functional;

// Traditional approach with exceptions
public User GetUser(int id)
{
    var user = _repository.Find(id);
    if (user is null)
        throw new NotFoundException($"User {id} not found");
    return user;
}

// Functional approach with Result<T>
public Result<User> GetUser(int id)
{
    var user = _repository.Find(id);
    return user is not null
        ? Result.Success(user)
        : Result.Failure<User>(new NotFoundFailure($"User {id} not found"));
}

Pattern Matching

var result = GetUser(42);

result.Match(
    success: user => Console.WriteLine($"Found: {user.Name}"),
    failure: failure => Console.WriteLine($"Failure: {failure.Message}")
);

Chaining Operations (Railway-Oriented Programming)

public Result<Order> CreateOrder(int userId, OrderRequest request)
{
    return GetUser(userId)
        .Ensure(user => user.IsActive, new ValidationFailure("User is not active"))
        .Bind(user => ValidateOrder(request))
        .Bind(validOrder => SaveOrder(validOrder))
        .Tap(order => _eventBus.Publish(new OrderCreated(order.Id)))
        .WithMetadata("userId", userId);
}

Maybe<T> - Optional Values Without Null

public Maybe<User> FindUserByEmail(string email)
{
    var user = _repository.FindByEmail(email);
    return user is not null
        ? Maybe.Some(user)
        : Maybe.None<User>();
}

// Pattern matching
var maybeUser = FindUserByEmail("user@example.com");
maybeUser.Match(
    some: user => Console.WriteLine($"Found: {user.Name}"),
    none: () => Console.WriteLine("User not found")
);

// Convert to Result
var result = maybeUser.ToResult(new NotFoundFailure("User not found"));

OneOf<T> - Discriminated Unions

// Represent a value that can be one of several types
public OneOf<Success, ValidationFailure, NotFoundFailure> ProcessRequest(int id)
{
    if (id <= 0)
        return new ValidationFailure("Invalid ID");

    var item = _repository.Find(id);
    if (item is null)
        return new NotFoundFailure($"Item {id} not found");

    return new Success();
}

// Pattern match on all cases
var response = ProcessRequest(id);
response.Match(
    first: success => Ok(),
    second: validation => BadRequest(validation.Message),
    third: notFound => NotFound(notFound.Message)
);

๐Ÿ“š Core Concepts

Result<T> Operations

Transformation
// Map - Transform success value
Result<int> result = Result.Success(5);
Result<int> doubled = result.Map(x => x * 2); // Success(10)

// Bind - Chain operations that return Result
Result<User> userResult = GetUser(id);
Result<Order> orderResult = userResult.Bind(user => CreateOrder(user));

// Flatten - Unwrap nested results
Result<Result<int>> nested = Result.Success(Result.Success(42));
Result<int> flat = nested.Flatten(); // Success(42)
Failure Handling
// Recover - Provide fallback on failure
Result<User> result = GetUser(id)
    .Recover(failure => GetDefaultUser());

// Ensure - Add validation
Result<User> result = GetUser(id)
    .Ensure(user => user.Age >= 18, new ValidationFailure("Must be 18+"));
Side Effects
// Tap - Execute side effect on success
Result<Order> result = CreateOrder(request)
    .Tap(order => _logger.LogInformation($"Order {order.Id} created"));

// TapFailure - Execute side effect on failure
Result<Order> result = CreateOrder(request)
    .TapFailure(failure => _logger.LogError($"Failed: {failure.Message}"));

// TapBoth - Execute side effect regardless of result
Result<Order> result = CreateOrder(request)
    .TapBoth(
        success: order => _metrics.RecordSuccess(),
        failure: failure => _metrics.RecordFailure()
    );
Value Access
// TryGet - Safe value extraction
if (result.TryGet(out var value, out var failure))
{
    Console.WriteLine($"Success: {value}");
}
else
{
    Console.WriteLine($"Failure: {failure.Message}");
}

// ValueOr - Provide default value
var user = result.ValueOr(GetDefaultUser());

// ValueOrThrow - Get value or throw
var user = result.ValueOrThrow(); // Throws if failed

Failure Types

// Basic failure
var failure = new Failure("Something went wrong");

// Validation failure with field details
var validationFailure = new ValidationFailure("Invalid input", new Dictionary<string, object?>
{
    ["Email"] = "Invalid email format",
    ["Age"] = "Must be at least 18"
});

// Not found failure
var notFoundFailure = new NotFoundFailure("User not found");

// Conflict failure
var conflictFailure = new ConflictFailure("Email already exists");

// Unauthorized failure
var unauthorizedFailure = new UnauthorizedFailure("Access denied");

// Exceptional failure (wrap exceptions)
try
{
    // risky operation
}
catch (Exception ex)
{
    return Result.Failure(new ExceptionalFailure(ex));
}

// Aggregate failure (multiple failures)
var failures = new List<Failure>
{
    new ValidationFailure("Invalid name"),
    new ValidationFailure("Invalid email")
};
var aggregateFailure = new AggregateFailure(failures);

Metadata

Attach contextual information to results:

var result = Result.Success(user)
    .WithMetadata("requestId", requestId)
    .WithMetadata("timestamp", DateTime.UtcNow)
    .WithMetadata("source", "UserService");

// Access metadata
if (result.Metadata.TryGetValue("requestId", out var requestId))
{
    _logger.LogInformation($"Request {requestId} completed");
}

// Fluent builder
var result = Result.Success(user)
    .WithMetadata(builder => builder
        .Add("requestId", requestId)
        .Add("duration", stopwatch.ElapsedMilliseconds)
        .Add("cacheHit", false));

Async Support

All operations work seamlessly with Task<T> and ValueTask<T>:

public async Task<Result<Order>> CreateOrderAsync(int userId, OrderRequest request)
{
    return await GetUserAsync(userId)
        .Bind(user => ValidateOrderAsync(request))
        .Bind(validOrder => SaveOrderAsync(validOrder))
        .Tap(order => PublishEventAsync(order));
}

// Works with ValueTask too
public async ValueTask<Result<User>> GetUserAsync(int id)
{
    var user = await _repository.FindAsync(id);
    return user is not null
        ? Result.Success(user)
        : Result.Failure<User>(new NotFoundFailure($"User {id} not found"));
}

๐ŸŒ ASP.NET Core Integration

using UnambitiousFx.Functional.AspNetCore;

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    // Automatically converts Result to appropriate HTTP response
    [HttpGet("{id}")]
    public async Task<IActionResult> GetUser(int id)
    {
        return await _userService
            .GetUserAsync(id)
            .ToHttpResult(); // 200 OK or 404 Not Found
    }

    [HttpPost]
    public async Task<IActionResult> CreateUser(CreateUserRequest request)
    {
        return await _userService
            .CreateUserAsync(request)
            .ToHttpResult(); // 200 OK, 400 Bad Request, or 409 Conflict
    }
}

Configure failure mapping:

services.AddResultHttp(options =>
{
    options.AddMapper<ValidationFailure>(StatusCodes.Status400BadRequest);
    options.AddMapper<NotFoundFailure>(StatusCodes.Status404NotFound);
    options.AddMapper<ConflictFailure>(StatusCodes.Status409Conflict);
    options.AddMapper<UnauthorizedFailure>(StatusCodes.Status401Unauthorized);
});

๐Ÿงช Testing with xUnit

using UnambitiousFx.Functional.xunit;

[Fact]
public void CreateUser_WithValidData_ReturnsSuccess()
{
    // Arrange
    var request = new CreateUserRequest { Name = "John", Email = "john@example.com" };

    // Act
    var result = _service.CreateUser(request);

    // Assert
    result.Should().BeSuccess()
        .Which(user => user.Name.Should().Be("John"));
}

[Fact]
public void CreateUser_WithInvalidEmail_ReturnsValidationFailure()
{
    // Arrange
    var request = new CreateUserRequest { Name = "John", Email = "invalid" };

    // Act
    var result = _service.CreateUser(request);

    // Assert
    result.Should().BeFailure()
        .WithError<ValidationFailure>()
        .Which(failure => failure.Message.Should().Contain("email"));
}

[Fact]
public void FindUser_WhenNotExists_ReturnsNone()
{
    // Act
    var maybe = _repository.FindByEmail("notfound@example.com");

    // Assert
    maybe.Should().BeNone();
}

๐ŸŽ“ Philosophy

This library embraces:

  • Railway-Oriented Programming - Operations form a "railway track" where success continues down the happy path and failures short-circuit to the failure path
  • Explicit Failure Handling - Failures are values, not exceptions. They're part of your type signatures
  • Composition Over Conditionals - Chain operations naturally using Bind, Map, and other combinators
  • Type Safety - Compiler enforces failure handling, reducing runtime surprises
  • Functional Core, Imperative Shell - Pure functional core with pragmatic integration for .NET ecosystem

๐Ÿ“Š Performance

We take performance seriously. Check our benchmarks comparing against:

  • Traditional exception-based code
  • FluentResults
  • Ardalis.Result

Key highlights:

  • Zero allocations for success paths in many scenarios
  • readonly struct value types minimize heap pressure
  • Lazy failure aggregation
  • Optimized async state machines

๐Ÿค Contributing

We welcome contributions! Please read our Contributing Guide for details on:

  • Code of conduct
  • Development setup
  • Coding standards
  • Testing requirements
  • Pull request process

Check out our good first issues to get started!

๐Ÿ“ Release notes

See the Releases page on GitHub for detailed release notes and version history: https://github.com/UnambitiousFx/Functional/releases

๐Ÿ“„ License

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

๐Ÿ™ Acknowledgments

Inspired by:

๐Ÿ“š Further Reading


Made with โค๏ธ by the UnambitiousFx team

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 (3)

Showing the top 3 NuGet packages that depend on UnambitiousFx.Functional:

Package Downloads
UnambitiousFx.Functional.AspNetCore

A lightweight functional programming library for .NET providing Result and Maybe types for railway-oriented programming and error handling.

UnambitiousFx.Functional.xunit

A lightweight functional programming library for .NET providing Result and Maybe types for railway-oriented programming and error handling.

UnambitiousFx.Synapse.Abstractions

A lightweight, performance-oriented library for building message-driven applications and in-process mediators. Provides composable primitives for Commands, Queries, Events, and Pipelines with a focus on low latency and minimal allocations.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.0 127 4/8/2026
1.0.6 156 1/18/2026
1.0.5 143 1/16/2026
1.0.4 136 1/12/2026
1.0.3 147 1/9/2026
1.0.2 138 1/7/2026
1.0.1 140 1/6/2026
1.0.0 235 1/3/2026