UnambitiousFx.Functional
2.0.0
dotnet add package UnambitiousFx.Functional --version 2.0.0
NuGet\Install-Package UnambitiousFx.Functional -Version 2.0.0
<PackageReference Include="UnambitiousFx.Functional" Version="2.0.0" />
<PackageVersion Include="UnambitiousFx.Functional" Version="2.0.0" />
<PackageReference Include="UnambitiousFx.Functional" />
paket add UnambitiousFx.Functional --version 2.0.0
#r "nuget: UnambitiousFx.Functional, 2.0.0"
#:package UnambitiousFx.Functional@2.0.0
#addin nuget:?package=UnambitiousFx.Functional&version=2.0.0
#tool nuget:?package=UnambitiousFx.Functional&version=2.0.0
UnambitiousFx.Functional
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 exceptionsMaybe<T>- Type-safe optional values, no more null reference exceptionsOneOf<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>andValueTask<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 structvalue 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:
- Railway Oriented Programming by Scott Wlaschin
- F# Result type
- Rust's Result and Option types
- LanguageExt
- FluentResults
๐ Further Reading
Made with โค๏ธ by the UnambitiousFx team
| Product | Versions 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. |
-
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.