Cirreum.Result
1.0.15
dotnet add package Cirreum.Result --version 1.0.15
NuGet\Install-Package Cirreum.Result -Version 1.0.15
<PackageReference Include="Cirreum.Result" Version="1.0.15" />
<PackageVersion Include="Cirreum.Result" Version="1.0.15" />
<PackageReference Include="Cirreum.Result" />
paket add Cirreum.Result --version 1.0.15
#r "nuget: Cirreum.Result, 1.0.15"
#:package Cirreum.Result@1.0.15
#addin nuget:?package=Cirreum.Result&version=1.0.15
#tool nuget:?package=Cirreum.Result&version=1.0.15
Cirreum Result: High-Performance Railway-Oriented Programming for C#
Overview
A lightweight, allocation‑free, struct‑based Result and Optional monad library designed for high‑performance .NET applications.
Provides a complete toolkit for functional, exception‑free control flow with full async support, validation, inspection, monadic composition, and pagination result types.
✨ Features
- Struct-based: No heap allocations on the success path.
- Unified Success/Failure Model using
ResultandResult<T>. - Optional Values with
Optional<T>for explicit presence/absence modeling. - Pagination Results with
SliceResult<T>,CursorResult<T>, andPagedResult<T>for consistent data access patterns. - Full async support
ValueTask+Task- Async variants of
Map,Then,Ensure,Inspect, failure handlers, etc.
- Comprehensive validation pipeline with
Ensure:- Three overload patterns: error message, direct exception, error factory
- Full async support for async predicates
- Chainable validation with fail-fast semantics
- Inspection helpers:
Inspect,InspectTry. - Composable monad API:
Map,Then,Match,Switch,TryGetValue,TryGetError, and more. - Ergonomic extension methods for async workflows.
- Zero exceptions for control flow—exceptions are captured as failures.
📦 Installation
dotnet add package Cirreum.Result
🧱 Target Frameworks
Result is built for modern, high-performance .NET, with first-class support for:
| TFM | Status | Notes |
|---|---|---|
| .NET 10 | ✔️ Primary | Latest runtime/JIT optimizations. Best performance. |
| .NET 9 | ✔️ Supported | Fully compatible. Same performance envelope for most scenarios. |
| .NET 8 (LTS) | ✔️ Supported | Ideal for production stability; fully compatible. |
🚀 Quick Start
Basic Success / Failure
Result SaveUser(User user)
{
if (user is null)
return Result.Fail(new ArgumentNullException(nameof(user)));
return Result.Success;
}
Result<User> CreateUser(string name)
{
if (string.IsNullOrWhiteSpace(name))
return Result<User>.Fail(new ArgumentException("Name is required"));
return Result<User>.Success(new User(name));
}
🔗 Chaining With Then and Map
var result =
CreateUser("Alice")
.Map(user => user.Id)
.Then(LogCreation);
🏭 Static Factory Methods
The non-generic Result class provides convenient factory methods:
// Create a successful Result<T>
var success = Result.From(42); // Result<int>
// Create a failed Result<T>
var failure = Result.Fail<string>(new InvalidOperationException("Error"));
// Convert Optional<T> to Result<T>
var optional = Optional<User>.From(user);
// With direct exception
var result1 = Result.FromOptional(optional, new NotFoundException("User not found"));
// With error message (creates InvalidOperationException)
var result2 = Result.FromOptional(optional, "User not found");
// With error factory for lazy evaluation
var result3 = Result.FromOptional(optional, () => new NotFoundException($"User {id} not found"));
🎯 Optional Values
Use Optional<T> when a value may be absent without implying an error:
Creating Optionals
// When you know the value is not null
var name = Optional<string>.For("John");
// When the value might be null (null-safe)
var user = _db.Users.Find(id);
var userOptional = Optional.From(user); // Returns Empty if null
// Explicit empty
var empty = Optional<int>.Empty;
Using Optionals
Optional<User> FindUser(int id)
{
var user = _db.Users.Find(id);
return Optional.From(user); // Returns Empty if null
}
// Chaining
var displayName = FindUser(id)
.Map(u => u.DisplayName)
.GetValueOrDefault("Unknown");
// Pattern matching
FindUser(id).Switch(
onValue: user => Console.WriteLine($"Found: {user.Name}"),
onEmpty: () => Console.WriteLine("User not found"));
// Convert to Result when absence is an error
var result = FindUser(id)
.ToResult(new NotFoundException("User not found"));
// Or use the static factory method
var result2 = Result.FromOptional(FindUser(id), new NotFoundException("User not found"));
When to Use Optional<T> vs Result<T>
| Use Case | Type |
|---|---|
| Operation that might fail with a reason | Result<T> |
| Value that may or may not exist | Optional<T> |
| Dictionary/cache lookup | Optional<T> |
| Database query that might return null | Optional<T> |
| Validation or business rule failure | Result<T> |
| API call that might error | Result<T> |
📄 Pagination Results
Three result types provide consistent contracts for paginated data across any persistence implementation (SQL, NoSQL, APIs, etc.):
SliceResult<T>
The simplest pagination type—just items and a "has more" indicator. Ideal for "load more" buttons or batch processing.
// Creating a slice
var slice = new SliceResult<Order>(orders, hasMore: true);
// Empty slice
var empty = SliceResult<Order>.Empty;
// Transform items while preserving metadata
var dtos = slice.Map(order => new OrderDto(order));
// Check state
if (slice.IsEmpty) { /* handle empty */ }
if (slice.HasMore) { /* show "Load More" button */ }
CursorResult<T>
Cursor-based (keyset) pagination for stable results across data changes. Ideal for infinite scroll, real-time data, and large datasets.
// Creating a cursor result
var result = new CursorResult<Order>(orders, nextCursor: "abc123", hasNextPage: true) {
PreviousCursor = "xyz789",
TotalCount = 1000 // Optional
};
// Empty result
var empty = CursorResult<Order>.Empty;
// Transform items
var dtos = result.Map(order => new OrderDto(order));
// Navigation
if (result.HasNextPage) { /* use result.NextCursor */ }
if (result.HasPreviousPage) { /* use result.PreviousCursor */ }
PagedResult<T>
Offset-based pagination with full metadata. Ideal for traditional paged UIs with page numbers.
// Creating a paged result
var result = new PagedResult<Order>(orders, totalCount: 100, pageSize: 25, pageNumber: 1);
// Empty result with custom page size
var empty = PagedResult<Order>.Empty(pageSize: 50);
// Transform items
var dtos = result.Map(order => new OrderDto(order));
// Navigation and metadata
Console.WriteLine($"Page {result.PageNumber} of {result.TotalPages}");
Console.WriteLine($"Showing {result.Count} of {result.TotalCount} items");
if (result.HasNextPage) { /* show next button */ }
if (result.HasPreviousPage) { /* show previous button */ }
When to Use Each Pagination Type
| Use Case | Type | Why |
|---|---|---|
| "Load more" button | SliceResult<T> |
Simple, no count query needed |
| Infinite scroll | CursorResult<T> |
Stable with data changes |
| Large datasets | CursorResult<T> |
Consistent performance at any depth |
| Real-time data | CursorResult<T> |
No shifting results |
| Traditional paged UI | PagedResult<T> |
Users expect page numbers |
| Small datasets with page jumps | PagedResult<T> |
Random page access needed |
| Batch processing | SliceResult<T> |
Minimal overhead |
Transforming Results Across Layers
All pagination types support Map() for clean DTO projections:
// Repository returns domain entities
public async Task<PagedResult<Order>> GetOrdersAsync(int page, int pageSize)
{
// ... query implementation
}
// Service transforms to DTOs
public async Task<PagedResult<OrderDto>> GetOrderDtosAsync(int page, int pageSize)
{
var orders = await _repository.GetOrdersAsync(page, pageSize);
return orders.Map(order => new OrderDto(order));
}
// Controller returns the result directly
[HttpGet]
public async Task<PagedResult<OrderDto>> GetOrders(int page = 1, int pageSize = 25)
{
return await _orderService.GetOrderDtosAsync(page, pageSize);
}
✅ Validation With Ensure
The Ensure method provides a fluent way to add validation to your Result<T> pipeline. If the predicate returns false, the success result is converted to a failure.
Synchronous Ensure
// With error message (creates InvalidOperationException)
var result = GetOrder(id)
.Ensure(o => o.Amount > 0, "Amount must be positive");
// With exception factory for custom error types
var result = GetOrder(id)
.Ensure(o => o.Amount > 0, o => new ValidationException($"Order {o.Id} has invalid amount: {o.Amount}"));
// With direct exception
var result = GetOrder(id)
.Ensure(o => o.Amount > 0, new ValidationException("Amount must be positive"));
// Chain multiple validations - stops on first failure
var result = GetOrder(id)
.Ensure(o => o.Amount > 0, "Amount must be positive")
.Ensure(o => o.Items.Any(), "Order must have items")
.Ensure(o => o.CustomerId != null, o => new InvalidOperationException($"Order {o.Id} has no customer"));
Async Ensure
// Async predicate with error factory
var result = await GetOrderAsync(id)
.EnsureAsync(async o => await IsValidCustomer(o.CustomerId),
o => new ValidationException($"Invalid customer: {o.CustomerId}"));
// Async predicate with direct exception
var result = await GetOrderAsync(id)
.EnsureAsync(async o => await HasSufficientStock(o.Items),
new InsufficientStockException());
// Mix sync and async validations
var result = await GetOrderAsync(id)
.EnsureAsync(o => o.Amount > 0, "Amount must be positive") // sync predicate
.EnsureAsync(async o => await IsValidCustomer(o.CustomerId), "Invalid customer") // async predicate
.EnsureAsync(o => o.Items.All(i => i.Quantity > 0), "All items must have positive quantity");
👀 Side‑Effects With Inspect
result
.Inspect(r => logger.LogInformation("Result: {State}", r.IsSuccess ? "OK" : "FAIL"));
⚡ Async Support (ValueTask + Task)
Every operation has async variants:
var result =
await GetUserAsync(id)
.OnSuccessAsync(user => logger.LogInformation("Loaded {Id}", user.Id))
.OnFailureAsync(ex => logger.LogError(ex, "Failed to load user"));
Or with async lambdas:
await SaveAsync(entity)
.OnSuccessTryAsync(async () => await NotifyAsync(entity));
🧩 Pattern Matching
var message = result.Match(
onSuccess: () => "OK",
onFailure: ex => $"Error: {ex.Message}");
🏗️ API Overview
Result (non-generic)
IsSuccess,IsFailure,ErrorOnSuccess,OnFailure,Inspect,TryGetErrorMap,Then,Match- Static factories:
From<T>(T value)- createsResult<T>Fail<T>(Exception error)- creates failedResult<T>FromOptional<T>(Optional<T>, Exception/string/Func<Exception>)- convertsOptional<T>toResult<T>
- Async:
OnSuccessAsync,OnFailureAsync,SwitchAsync, etc.
Result<T>
All of the above, plus:
Value/TryGetValueMap<TOut>(...)Ensure(...)validation helpers:Ensure(Func<T, bool> predicate, string errorMessage)Ensure(Func<T, bool> predicate, Exception error)Ensure(Func<T, bool> predicate, Func<T, Exception> errorFactory)
Optional<T>
HasValue,IsEmpty,Value,TryGetValueFor(T non-null)- creates Optional from non-null valueEmpty- static property for empty optionalMap,Then,Match,Switch,WhereGetValueOrDefault(T),GetValueOrDefault(Func<T>),GetValueOrNull()ToResult(Exception)for converting toResult<T>
Optional (non-generic factory)
From<T>(T?)- null-safe factory method
SliceResult<T>
Items,HasMore,Count,IsEmptyEmpty- static property for empty sliceMap<TResult>(Func<T, TResult>)- transform items preserving metadata
CursorResult<T>
Items,NextCursor,HasNextPage,Count,IsEmptyPreviousCursor,HasPreviousPage- bidirectional navigationTotalCount- optional total countEmpty- static property for empty resultMap<TResult>(Func<T, TResult>)- transform items preserving metadata
PagedResult<T>
Items,TotalCount,PageSize,PageNumber,Count,IsEmptyTotalPages,HasNextPage,HasPreviousPage- computed propertiesEmpty(int pageSize = 25)- static factory for empty resultMap<TResult>(Func<T, TResult>)- transform items preserving metadata
Async Extensions
(From ResultAsyncExtensions)
Supports async versions of:
OnSuccessOnSuccessTryOnFailureOnFailureTryInspectEnsureMapThenMatch
All support both ValueTask and Task.
🧪 Example: End‑to‑End Pipeline
var result =
await Validate(request)
.Then(() => LoadUser(request.UserId))
.Ensure(u => u.IsActive, "User must be active")
.Map(user => user.Profile)
.OnSuccessAsync(profile => Cache(profile))
.OnFailureAsync(ex => LogFailure(ex));
No exceptions. No branches. Pure railway flow.
📚 Real-World Examples
User Registration with Validation
public async Task<Result<User>> RegisterUserAsync(RegistrationRequest request)
{
return await Result<RegistrationRequest>.Success(request)
.Ensure(r => !string.IsNullOrWhiteSpace(r.Email), "Email is required")
.Ensure(r => IsValidEmail(r.Email), "Invalid email format")
.Ensure(r => r.Password.Length >= 8, "Password must be at least 8 characters")
.EnsureAsync(async r => !await UserExists(r.Email),
r => new DuplicateUserException($"User with email {r.Email} already exists"))
.Map(r => new User { Email = r.Email, PasswordHash = HashPassword(r.Password) })
.ThenAsync(async user => await SaveUserAsync(user))
.OnSuccessAsync(async user => await SendWelcomeEmailAsync(user.Email))
.InspectAsync(result =>
_logger.LogInformation("Registration attempt for {Email}: {Success}",
request.Email, result.IsSuccess));
}
Order Processing with Stock Validation
public async Task<Result<Order>> ProcessOrderAsync(OrderRequest request)
{
return await LoadCustomerAsync(request.CustomerId)
.Ensure(c => c.IsActive, c => new InactiveCustomerException($"Customer {c.Id} is inactive"))
.Ensure(c => !c.HasOutstandingBalance, "Customer has outstanding balance")
.Map(customer => CreateOrder(customer, request.Items))
.EnsureAsync(async order => await CheckInventoryAsync(order.Items),
"Insufficient inventory for one or more items")
.Ensure(order => order.Total >= 10m, "Minimum order amount is $10")
.ThenAsync(async order => await SaveOrderAsync(order))
.OnSuccessAsync(async order => {
await ReserveInventoryAsync(order.Items);
await NotifyWarehouseAsync(order);
})
.OnFailureAsync(async error => {
await _logger.LogErrorAsync(error, "Order processing failed");
if (error is InsufficientInventoryException)
await NotifyInventoryTeamAsync(request);
});
}
API Response Handling with Optional
public async Task<Result<UserProfile>> GetUserProfileAsync(int userId)
{
// Find user returns Optional<User>
var userOptional = await _repository.FindUserAsync(userId);
return userOptional
.ToResult(new NotFoundException($"User {userId} not found"))
.EnsureAsync(async u => await IsAuthorizedToViewProfile(u),
new UnauthorizedException("Not authorized to view this profile"))
.ThenAsync(async user => {
var profile = await LoadProfileAsync(user.Id);
var preferences = await LoadPreferencesAsync(user.Id);
return BuildCompleteProfile(user, profile, preferences);
});
}
// Alternative using static factory
public async Task<Result<Product>> GetProductAsync(string sku)
{
var productOptional = await _cache.GetProductAsync(sku);
// Convert optional to result using factory method
return Result.FromOptional(productOptional, () => new ProductNotFoundException($"Product {sku} not found"))
.Ensure(p => p.IsAvailable, "Product is not available")
.Ensure(p => p.Stock > 0, p => new OutOfStockException($"Product {p.Name} is out of stock"))
.Map(p => ApplyCurrentPricing(p));
}
Paginated API with DTO Transformation
public async Task<PagedResult<OrderSummaryDto>> GetOrdersAsync(
Guid customerId,
int pageNumber = 1,
int pageSize = 25)
{
var orders = await _repository.GetOrdersByCustomerAsync(customerId, pageNumber, pageSize);
return orders.Map(order => new OrderSummaryDto {
Id = order.Id,
OrderDate = order.CreatedAt,
Total = order.Total,
Status = order.Status.ToString(),
ItemCount = order.Items.Count
});
}
// Cursor-based for infinite scroll
public async Task<CursorResult<ActivityDto>> GetActivityFeedAsync(
Guid userId,
string? cursor = null,
int pageSize = 50)
{
var activities = await _repository.GetActivitiesAsync(userId, cursor, pageSize);
return activities.Map(activity => new ActivityDto {
Id = activity.Id,
Type = activity.Type,
Description = activity.Description,
Timestamp = activity.CreatedAt
});
}
💡 Best Practices
1. Use Specific Exception Types
// Good - specific exception with context
.Ensure(order => order.Items.Any(),
order => new EmptyOrderException($"Order {order.Id} has no items"))
// Less ideal - generic exception
.Ensure(order => order.Items.Any(), "Order has no items")
2. Chain Validations from General to Specific
// Good - fails fast on basic validation
result
.Ensure(x => x != null, "Value cannot be null")
.Ensure(x => x.Length > 0, "Value cannot be empty")
.Ensure(x => x.Length <= 100, "Value too long")
.EnsureAsync(async x => await IsUniqueAsync(x), "Value must be unique");
3. Use Optional<T> for Lookups, Result<T> for Operations
// Good
Optional<User> FindUserByEmail(string email);
Result<User> CreateUser(CreateUserRequest request);
// Avoid
Result<User> FindUserByEmail(string email); // Finding nothing isn't an error
Optional<User> CreateUser(CreateUserRequest request); // Creation can fail
4. Keep Async and Sync Separate
// Good - clear async boundaries
var result = ProcessSync()
.Then(x => TransformSync(x))
.ThenAsync(async x => await SaveAsync(x))
.OnSuccessAsync(async x => await NotifyAsync(x));
// Avoid - mixing unnecessarily
var result = await ProcessSync()
.ThenAsync(async x => TransformSync(x)) // Wrapping sync in async
5. Use Error Context in Factories
// Good - provides context
.Ensure(u => u.Age >= 18,
u => new ValidationException($"User {u.Id} is underage: {u.Age}"))
// Less helpful
.Ensure(u => u.Age >= 18, new ValidationException("User is underage"))
6. Choose the Right Pagination Type
// Good - cursor for real-time feed
public Task<CursorResult<Post>> GetTimelineAsync(string? cursor);
// Good - paged for admin dashboard
public Task<PagedResult<User>> GetUsersAsync(int page, int pageSize);
// Good - slice for "show more" preview
public Task<SliceResult<Comment>> GetRecentCommentsAsync(int limit);
// Avoid - offset pagination for large real-time data
public Task<PagedResult<Post>> GetTimelineAsync(int page); // Results shift as new posts arrive
🛠️ Why Struct‑Based?
- Avoids heap allocations
- No capturing of closures on success path
- More predictable performance in hot loops
- Perfect for high‑volume messaging, pipelines, middleware, and game engines
📜 License
This project is licensed under the MIT License.
🤝 Contributing
Pull requests are welcome! If you have ideas for improvements—performance tweaks, new helpers, analyzers—feel free to open an issue or contribute directly.
🧭 Project Files
- Core interfaces:
IResult,IResult<T> - Implementations:
Result,Result<T>,Optional<T> - Pagination:
SliceResult<T>,CursorResult<T>,PagedResult<T> - Async pipeline operators:
ResultAsyncExtensions - Monadic composition, validation, and inspection APIs
- Designed to support any .NET hosting model (Server, WASM, Azure Functions)
Made with ❤️ for clean, predictable, exception‑free flow control.
Cirreum Foundation Framework
Layered simplicity for modern .NET
| 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 Cirreum.Result:
| Package | Downloads |
|---|---|
|
Cirreum.Core
The Cirreum Application Core libary |
|
|
Cirreum.Persistence.Sql
Dapper persistence provider for Cirreum framework |
|
|
Cirreum.Persistence.NoSql
The Cirreum Persistence NoSql Library. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.15 | 1,970 | 3/13/2026 |
| 1.0.14 | 489 | 3/6/2026 |
| 1.0.11 | 1,191 | 1/21/2026 |
| 1.0.10 | 802 | 1/5/2026 |
| 1.0.9 | 286 | 1/3/2026 |
| 1.0.8 | 111 | 1/3/2026 |
| 1.0.7 | 102 | 1/3/2026 |
| 1.0.6 | 341 | 1/2/2026 |
| 1.0.5 | 271 | 12/31/2025 |
| 1.0.4 | 1,169 | 12/20/2025 |
| 1.0.3 | 593 | 12/16/2025 |
| 1.0.2 | 1,448 | 11/19/2025 |
| 1.0.1 | 434 | 11/17/2025 |