You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Generic Result pattern and functional primitives for .NET. Provides railway-oriented programming extensions, typed error handling, and pattern matching support with zero dependencies and zero business logic — can be used in any .NET application.
Features
Result<T> & Result — Type-safe success/failure for value-returning and void-like operations
Typed Error Hierarchy — Error, ErrorCode, ValidationError, ValidationFailure with metadata, inner errors, and field-level details
Implicit Conversions — Ergonomic syntax: assign values or errors directly to Result<T> / Result
Async Pipeline Support — Full async extensions for Task<Result<T>> and Task<Result> composition
Pattern Matching — Match (with return) and Switch (side-effects) for exhaustive handling
Collection Operations — Collect to aggregate IEnumerable<Result<T>> and Combine for tuples
Try/Catch Wrapping — Result.Try() / Result.TryAsync() to convert exception-based code to Results
Nullable Bridging — Result.FromNullable() for reference types and nullable value types
Deconstruct & Bridging — Deconstruct for tuple-style consumption, ToResult() / Map<T> / Bind<T> to bridge between Result and Result<T>
Zero Dependencies — 100% standalone, no external NuGet packages
.NET 10.0+ / C# 14 — Modern language features including extension members
Source Link Enabled — Step into source code when debugging NuGet package
211+ Unit Tests — Comprehensive coverage across all types and operations
Installation
dotnet add package Clywell.Primitives
Quick Start
usingClywell.Primitives;// ── Value-returning operations: Result<T> ──────────────────────// Implicit conversions — no factory neededResult<int>success=42;Result<int>failure=Error.NotFound("User not found.");// Factory methodsvarok=Result.Success(42);varfail=Result.Failure<int>(Error.Conflict("Already exists"));// ── Void-like operations: Result ───────────────────────────────Resultdeleted=Result.Success();ResultnotFound=Result.Failure(Error.NotFound("Item not found"));ResultfromError=Error.Forbidden("Not allowed");// implicit conversion// ── Pattern matching ───────────────────────────────────────────stringmessage=ok.Match(onSuccess: value =>$"Got: {value}",onFailure: error =>$"Failed: {error.Description}");deleted.Switch(onSuccess:()=>Console.WriteLine("Done"),onFailure: error =>Console.WriteLine($"Error: {error}"));// ── Railway-oriented pipeline ──────────────────────────────────varresult=Result.Success("42").Map(int.Parse)// string → int.Ensure(v =>v>0,Error.Failure("Must be > 0"))// validate.Bind(v =>LookupUser(v))// int → Result<User>.Tap(user =>Console.WriteLine(user.Name))// side effect.MapError(e =>Error.Unexpected(e.Description))// remap error.Map(user =>user.Email);// User → string
Core Types
ErrorCode
A readonly record struct classifying error categories. Supports implicit conversion to/from string for extensibility.
Code
Value
Usage
ErrorCode.Failure
"General.Failure"
General/unspecified failure
ErrorCode.Validation
"General.Validation"
Validation rule violations
ErrorCode.NotFound
"General.NotFound"
Resource not found
ErrorCode.Conflict
"General.Conflict"
Duplicate/conflict scenarios
ErrorCode.Unauthorized
"General.Unauthorized"
Authentication failures
ErrorCode.Forbidden
"General.Forbidden"
Authorization/permission failures
ErrorCode.Unexpected
"General.Unexpected"
Internal/unexpected errors
ErrorCode.Unavailable
"General.Unavailable"
Service/resource unavailable
// Custom error codes via implicit conversionErrorCodecustom="Billing.PaymentDeclined";// String comparisonstringcode=ErrorCode.NotFound;// "General.NotFound"
Error
A record with Code, Description, optional InnerError, and Metadata. Immutable — builder methods return new instances.
Factory Methods
Error.Failure("Something went wrong")
Error.NotFound("User not found")
Error.Conflict("Email already registered")
Error.Unauthorized("Invalid credentials")
Error.Forbidden("Insufficient permissions")
Error.Unexpected("Unhandled exception occurred")
Error.Unavailable("Service temporarily down")
Error.Validation("Email","Email is required")// single field
Error.Validation(// multiple fieldsnewValidationFailure("Email","Required"),newValidationFailure("Age","Must be ≥ 18"))
Builder Methods (Immutable)
varerror=Error.NotFound("Order not found").WithMetadata("OrderId",orderId).WithMetadata("RequestId",correlationId).WithInnerError(originalError);// Bulk metadatavarenriched=error.WithMetadata(newDictionary<string,object>{["Timestamp"]=DateTime.UtcNow,["Retry"]=3});
Properties
Property
Type
Description
Code
ErrorCode
Categorized error code
Description
string
Human-readable error message
InnerError
Error?
Optional causal error (error chain)
Metadata
ImmutableDictionary<string,object>
Key-value pairs for context
ValidationFailure
A readonly record struct representing a single field-level validation failure.
varfailure=newValidationFailure("Email","Email is required");failure.FieldName;// "Email"failure.Message;// "Email is required"failure.ToString();// "Email: Email is required"
ValidationError
A sealed record extending Error with structured validation details. Always has ErrorCode.Validation.
varerror=Error.Validation(newValidationFailure("Email","Required"),newValidationFailure("Name","Too long"));error.Failures;// ImmutableArray<ValidationFailure>error.FailureCount;// 2error.HasFailureForField("Email");// trueerror.GetFailuresForField("Email");// IEnumerable<ValidationFailure>// Append failures (returns new instance)varcombined=error.AddFailures(newValidationFailure("Age","Must be positive"));
Result<T> — Value-Returning Operations
A readonly struct representing success with a TValue or failure with an Error. Implicit conversions allow assigning values and errors directly.
Creating
// Implicit conversionsResult<User>success=user;Result<User>failure=Error.NotFound("User not found");// Factory methodsResult.Success(user);Result.Failure<User>(error);Result.Failure<User>("Something went wrong");// shorthand for Error.Failure(...)
Properties
Property
Type
Description
IsSuccess
bool
true if the result contains a value
IsFailure
bool
true if the result contains an error
Value
T
The success value (throws if failure)
Error
Error
The error (throws if success)
Instance Methods
Method
Returns
Description
Match(onSuccess, onFailure)
TOut
Pattern match with return value
Switch(onSuccess, onFailure)
void
Pattern match with side effects
Map(fn)
Result<TOut>
Transform success value
Bind(fn)
Result<TOut>
Chain result-producing function (flatMap)
Tap(action)
Result<T>
Side effect on success
OnSuccess(action)
Result<T>
Execute action on success
OnFailure(action)
Result<T>
Execute action on failure
ValueOr(fallback)
T
Get value or fallback
ValueOr(fn)
T
Get value or compute fallback from error
ToResult()
Result
Discard value, preserve success/failure
Deconstruct
(bool, T?, Error?)
Tuple-style: var (ok, val, err) = result
MapAsync(fn)
Task<Result<TOut>>
Transform with async function
BindAsync(fn)
Task<Result<TOut>>
Chain with async result-producing function
TapAsync(fn)
Task<Result<T>>
Async side effect on success
TapErrorAsync(fn)
Task<Result<T>>
Async side effect on failure
MatchAsync(onSuccess, onFailure)
Task<TOut>
Pattern match with async functions
Extension Methods
Method
Returns
Description
Ensure(predicate, error)
Result<T>
Validate with predicate
Ensure(predicate, errorFactory)
Result<T>
Validate with lazy error from value
MapError(fn)
Result<T>
Transform the error
TapError(action)
Result<T>
Side effect on failure
Async Pipeline Extensions (on Task<Result<T>>)
These allow chaining directly off async operations without await:
A readonly struct for operations that succeed or fail but carry no value (e.g., Delete, SendEmail).
Creating
Resultsuccess=Result.Success();Resultfailure=Result.Failure(Error.NotFound("Item not found"));Resultquick=Result.Failure("Something went wrong");ResultfromErr=Error.Forbidden("Not allowed");// implicit conversion
publicasyncTask<Result>DeleteOrderAsync(intorderId){returnawaitFindOrder(orderId).Map(order =>order.Id).BindAsync(id =>repository.DeleteAsync(id)).Tap(()=>logger.LogInformation("Deleted order {Id}",orderId)).TapError(e =>logger.LogWarning("Delete failed: {Error}",e));}
API Controller
[HttpPost]publicIActionResultCreate(CreateUserRequestrequest){returnuserService.CreateUser(request).Match(onSuccess: user =>CreatedAtAction(nameof(Get),new{id=user.Id},user),onFailure: error =>error.Code.Valueswitch{"General.Validation"=>BadRequest(error),"General.Conflict"=>Conflict(error),"General.NotFound"=>NotFound(error),
_ =>StatusCode(500,error)});}
Combining Results
varcombined=Result.Combine(GetUser(userId),GetOrder(orderId),GetPayment(paymentId));returncombined.Map(tuple =>{var(user,order,payment)=tuple;returnnewOrderSummary(user.Name,order.Total,payment.Status);});// 4 & 5 result overloads available toovarall=Result.Combine(name,email,age,address);varfull=Result.Combine(name,email,age,address,phone);
Deconstructing Results
// Non-generic Resultvar(ok,error)=Result.Success();// ok = true, error = null// Generic Result<T>var(isSuccess,value,err)=Result.Success(42);// isSuccess = true, value = 42, err = null
Bridging Result ↔ Result<T>
// Result → Result<T> via Map/BindResultdeleted=DeleteOrder(id);Result<string>message=deleted.Map(()=>"Order deleted");Result<Order>order=deleted.Bind(()=>LoadOrder(id));// Result<T> → Result via ToResult (discards value)Result<User>userResult=GetUser(id);Resultplain=userResult.ToResult();
Error Enrichment
varerror=Error.NotFound("Order not found").WithMetadata("OrderId",orderId).WithInnerError(originalError);// Validation with rich detailsvarvalidation=Error.Validation(newValidationFailure("Email","Required"),newValidationFailure("Age","Must be 18 or older"));validation.Failures;// ImmutableArray<ValidationFailure>validation.HasFailureForField("Email");// true// Combine validation errorsvarmerged=validation.AddFailures(newValidationFailure("Name","Too long"));
Collecting Sequences
varresults=ids.Select(id =>ParseId(id));// IEnumerable<Result<int>>Result<IReadOnlyList<int>>all=results.Collect();// Success with all values, or first error encountered
// Reference typeResult<User>user=Result.FromNullable(repository.FindById(id),"User not found");// Nullable value typeResult<int>count=Result.FromNullable(GetOptionalCount(),// int?"Count unavailable");
Contributing
Fork the repository
Create a feature branch: git checkout -b feature/my-feature
Commit changes: git commit -m 'feat: add my feature'
Push to branch: git push origin feature/my-feature