EricksonLopez.Result.Serialization.Generators
2.0.0
dotnet add package EricksonLopez.Result.Serialization.Generators --version 2.0.0
NuGet\Install-Package EricksonLopez.Result.Serialization.Generators -Version 2.0.0
<PackageReference Include="EricksonLopez.Result.Serialization.Generators" Version="2.0.0"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
<PackageVersion Include="EricksonLopez.Result.Serialization.Generators" Version="2.0.0" />
<PackageReference Include="EricksonLopez.Result.Serialization.Generators"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
paket add EricksonLopez.Result.Serialization.Generators --version 2.0.0
#r "nuget: EricksonLopez.Result.Serialization.Generators, 2.0.0"
#:package EricksonLopez.Result.Serialization.Generators@2.0.0
#addin nuget:?package=EricksonLopez.Result.Serialization.Generators&version=2.0.0
#tool nuget:?package=EricksonLopez.Result.Serialization.Generators&version=2.0.0
<div align="center">
<img src="icon.png" alt="EricksonLopez.Result" width="120" />
EricksonLopez.Result
High-performance, struct-based, enterprise-grade Result Pattern and Railway-Oriented Programming ecosystem for modern .NET.
</div>
EricksonLopez.Result is a high-performance, struct-based, enterprise-grade Result Pattern and Railway-Oriented Programming ecosystem for modern .NET (.NET 8, .NET 9, .NET 10). Designed for mission-critical, high-throughput systems, it eliminates the CPU latency and heap allocation overhead of exception-driven control flow while providing a rich domain error taxonomy, RFC 9457 HTTP ProblemDetails mapping, distributed OpenTelemetry tracing, NativeAOT compliance, Roslyn compile-time analyzers, and fluent unit testing assertions.
Table of Contents
- What Problem It Solves
- Key Features
- Ecosystem
- Documentation
- Installation
- Quick Start
- Core Use Cases
- Use Case 1: Clean Architecture Application Services / CQRS
- Use Case 2: Multi-Step Domain Workflow with Short-Circuiting
- Use Case 3: Compound Form & Entity Validation
- Use Case 4: Repository Querying with Maybe<T>
- Use Case 5: Compile-Time Strongly-Typed Domain Error Hierarchies
- Use Case 6: Async Compound Validation with ValidateAllAsync
- Configuration & Integrations
- Testing & Quality
- Performance Benchmarks
- Compatibility & Technical Matrix
- Architecture & Design Principles
- Best Practices & Anti-Patterns
- Troubleshooting & Common Pitfalls
- Part of the EricksonLopez Ecosystem
- Contributing
- License
๐ฏ What Problem It Solves
In enterprise .NET applications, managing business rule violations, input validation failures, and asynchronous workflows using traditional patterns introduces critical architectural and performance drawbacks:
The Hidden Cost of Exceptions for Control Flow: Throwing exceptions for expected domain outcomes (e.g.,
UserNotFoundException,ValidationException) causes severe latency penalties due to stack trace captures, thread context switches, and GC heap allocations. It also creates hidden control flow pathways that bypass type signatures.Primitive Obsession & Loss of Context: Returning primitive flags (
bool,null, or tuple pairs) strips away domain diagnostics: error codes, severity levels, retryability hints, localized message keys, and distributed tracing IDs.Allocation Overhead in Existing Result Libraries: Many third-party Result libraries rely on heap-allocated
classwrappers, box value types during pipeline operations, allocate compiler closure objects on every lambda execution, or fail when compiled under NativeAOT and trimming.
How EricksonLopez.Result Solves This
- Zero-Allocation Execution: Core
ResultandResult<TValue>arereadonly structvalue types โ generating 0 bytes of heap allocation on happy paths. - Closure-Free Monadic Pipelines: Every combinator (
Map,Bind,Ensure,Tap,Match,Recover) includesTStateoverloads to completely eliminate lambda closure allocations. - Rich Domain Error Taxonomy: Sealed
Errorclass encapsulatesErrorType,ErrorSeverity,ErrorRetryability, lazy zero-allocation ambientTraceId, and localized resource keys (DescriptionKey). - Seamless Framework Integrations: Native mapping to ASP.NET Core RFC 9457 ProblemDetails, OpenTelemetry
ActivitySourcetracing, System.Text.Json source generation, and Roslyn compile-time analyzers.
โก Key Features
- ๐ Zero-Allocation Envelope: Core
ResultandResult<TValue>arereadonly structvalue types โ zero heap allocation for success results. - โก Closure-Free
TStatePipeline: All monadic operators (Map,Bind,TapOnSuccess,TapOnFailure,Match,Execute,Ensure,Recover) offerTStateoverloads to completely eliminate lambda closure allocations in hot execution paths. - ๐ Rich Enterprise Error Taxonomy: Sealed
Errorclass featuringErrorType,ErrorSeverity,ErrorRetryability, lazy zero-allocTraceId(ambientActivity),CorrelationId, localized keys (DescriptionKey- see i18n guide), and immutableMetadata. - ๐งฉ Span-Based
Result.Combine&Result.ValidateAll: Aggregates multiple validation failures or typed tuple results usingArrayPool<Error>to eliminate temporary array allocations. - ๐ ASP.NET Core RFC 9457 Integration: Automatic mapping of
Resultto HTTP responses (200 OK,400 Bad Request,401 Unauthorized,403 Forbidden,404 Not Found,409 Conflict,503 Unavailable,500 Server Error). - ๐ First-Class OpenTelemetry: BCL-only ambient trace capture (
Error.TraceId) and optionalActivitySourcemetrics counters viaEricksonLopez.Result.OpenTelemetry. - โก NativeAOT & Trimming Safe: Designed with zero reflection in hot paths, featuring source-generated
JsonSerializerContextdefinitions (IsAotCompatible=true). - ๐ก๏ธ Bundled Roslyn Diagnostic Analyzers: 11+ compile-time diagnostic rules (
RESULT001,RESULT003โRESULT010,RESULT012,RESULT_OTEL_001,RESULT_GEN_001) preventing performance degradation, uninitialized structs, and sensitive data leakage. - ๐งช Fluent Test Assertions: Declarative assertion API for unit testing with standard test frameworks (xUnit, NUnit, MSTest). Fully supports asynchronous
ValueTaskvalidation avoiding testing framework deadlocks.
๐ฆ Ecosystem
| Package | Version | Description |
|---|---|---|
EricksonLopez.Result |
Core struct-based Result, Error domain model, monadic pipeline, cumulative ValidateAll, LINQ support, and bundled Roslyn analyzers |
|
EricksonLopez.Result.Generic |
Strongly-typed Result<TValue, TError> with compile-time error types for strict domain model pipelines |
|
EricksonLopez.Result.Maybe |
Struct-based Maybe<T> option type for DDD repositories and query layers with seamless Result interop |
|
EricksonLopez.Result.AspNetCore |
ASP.NET Core Minimal APIs filter & RFC 9457 ProblemDetails HTTP response mapper | |
EricksonLopez.Result.OpenApi |
Minimal API OpenAPI metadata extensions (ProducesResult<T>()) for automated schema documentation |
|
EricksonLopez.Result.OpenTelemetry |
OpenTelemetry ActivitySource tracing integration and System.Diagnostics.Metrics counters (BCL-only) |
|
EricksonLopez.Result.Serialization |
System.Text.Json custom converters and NativeAOT trim-safe JsonSerializerContext |
|
EricksonLopez.Result.Serialization.Generators |
Roslyn source generator for AOT-compatible Result<T> JSON serialization |
|
EricksonLopez.Result.FluentValidation |
FluentValidation integration โ converts ValidationResult to structured Result failures |
|
EricksonLopez.Result.MediatR |
MediatR pipeline behavior โ catches unhandled exceptions and wraps them as Result failures |
|
EricksonLopez.Result.Testing |
Framework-agnostic fluent testing assertion library (ShouldBeSuccess(), ShouldHaveError()) |
|
EricksonLopez.Result.Testing.XUnit |
xUnit-specific test helpers โ assertion failures surface as XunitException |
|
EricksonLopez.Result.Testing.NUnit |
NUnit-specific test helpers โ assertion failures surface as AssertionException |
|
EricksonLopez.Result.Analyzers |
Roslyn analyzers & code fixes (RESULT001โ012) โ bundled with Core, also available standalone |
๐ Documentation
๐ Official Documentation & Hub: ericksonlopez.dev/result
๐ Step-by-Step Interactive Showcase (Levels 00 to 08)
| Level | Topic | Description |
|---|---|---|
| Level 00 | Architecture & Philosophy | Railway-Oriented Programming (ROP) vs Exceptions and zero-allocation struct guarantees |
| Level 01 | Getting Started & Primitives | Basic Result and Result<T> creation, error factories, and value unwrapping |
| Level 02 | Domain Modeling & Errors | Rich error taxonomy, severity, retryability, and lazy ambient trace correlation |
| Level 03 | Railway Pipelines & Monads | Monadic combinators (Bind, Map, Tap, Ensure), closure-free TState, and LINQ |
| Level 04 | Validation & Maybe Monad | Fail-all Result.ValidateAll aggregation and Maybe<T> option type interop |
| Level 05 | ASP.NET Core & RFC 9457 | Minimal APIs .ToHttpResult(), status code mapping, and transparent endpoint filters |
| Level 06 | Integrations & Analyzers | FluentValidation, MediatR pipeline behaviors, and Roslyn diagnostic rules (RESULT001โ012) |
| Level 07 | Native AOT & Serialization | Zero-reflection System.Text.Json source generation and trimming compliance |
| Level 08 | Telemetry & Fluent Testing | OpenTelemetry activity tracing, metrics counters, and fluent unit testing assertions |
๐ Technical Reference & Architecture Guides
- Architecture & Invariants โ Complete architectural blueprint, memory layouts, and domain boundaries.
- Architectural Decision Records (ADRs) โ 21 ADRs documenting design rationale and rejected proposals.
- Technical Audit โ Comprehensive technical audit, guarantees, and verification.
- Competitive Audit โ In-depth market comparison vs CSharpFunctionalExtensions, FluentResults, ErrorOr, OneOf, etc.
- Feature Catalog & Specs โ Exhaustive specification of all core types, monads, and extensions.
- Features & Compatibility Matrix โ Target framework matrix, diagnostics, and HTTP status codes.
- Testing & Quality Audit โ Verification topology, fast-path/slow-path testing, and mutation metrics.
- Best Practices Guide โ Recommended production patterns for microservices and domain logic.
- Anti-Patterns Guide โ Unsafe patterns, state bugs, and pitfalls to avoid.
- Cookbook & Recipes โ Ready-to-use recipes for ASP.NET Core, OpenTelemetry, testing, and LINQ syntax.
- Roslyn Analyzers Reference โ Detailed specifications and remediation steps for diagnostic rules RESULT001โ012.
- Internationalization (i18n) โ Multi-language localized error messages using
DescriptionKey. - Migration Guide โ Step-by-step guide for migrating from raw exceptions, FluentResults, or ErrorOr.
- Allocation Analysis โ Deep dive into memory benchmarks, struct layout, and zero-allocation mechanics.
- Mutation Score Report โ Detailed Stryker.NET mutation score verification across all 44 functional units.
- Package Reference โ Full dependency graph and per-package metadata for all 14 NuGet packages.
- CI/CD & Build Pipeline โ GitHub Actions workflows, automated releases, and supply chain security.
๐ฅ Installation
Install the required packages using the .NET CLI:
1. Core Package (Required)
# Core package includes struct-based Result, Error taxonomy, and bundled Roslyn Analyzers
dotnet add package EricksonLopez.Result
2. Optional Framework & Integration Packages
# Strongly-typed error pipeline (Result<TValue, TError>)
dotnet add package EricksonLopez.Result.Generic
# Option monad for domain queries (Maybe<T>)
dotnet add package EricksonLopez.Result.Maybe
# ASP.NET Core RFC 9457 ProblemDetails & Minimal API Filters
dotnet add package EricksonLopez.Result.AspNetCore
# Minimal API OpenAPI metadata extensions (ProducesResult<T>)
dotnet add package EricksonLopez.Result.OpenApi
# OpenTelemetry Activity tracing & BCL Metrics counters
dotnet add package EricksonLopez.Result.OpenTelemetry
# System.Text.Json converters & NativeAOT trimming support
dotnet add package EricksonLopez.Result.Serialization
# FluentValidation validation result mapping
dotnet add package EricksonLopez.Result.FluentValidation
# MediatR pipeline exception-handling behavior
dotnet add package EricksonLopez.Result.MediatR
3. Testing & Assertion Packages
# Framework-agnostic fluent testing assertions
dotnet add package EricksonLopez.Result.Testing
# xUnit-specific assertion helpers (surfaces XunitException)
dotnet add package EricksonLopez.Result.Testing.XUnit
# NUnit-specific assertion helpers (surfaces AssertionException)
dotnet add package EricksonLopez.Result.Testing.NUnit
๐ Quick Start
1. Core Result & Domain Errors
Define rich domain errors using semantic factory methods and return Result<T> instead of throwing exceptions:
using EricksonLopez.Result;
public static class UserErrors
{
public static Error NotFound(Guid id) =>
Error.NotFound("User.NotFound", $"User with ID '{id}' was not found.");
public static readonly Error InvalidEmail =
Error.Validation("User.InvalidEmail", "The email format is invalid.");
public static readonly Error Suspended =
Error.Forbidden("User.Suspended", "User account has been suspended.")
.WithRetryability(ErrorRetryability.Permanent);
}
public class UserService
{
private readonly IUserRepository _repository;
public UserService(IUserRepository repository) => _repository = repository;
public Result<User> GetUser(Guid id)
{
var user = _repository.Find(id);
return user is null
? UserErrors.NotFound(id)
: user; // Implicit conversion to Result<User>.Success(user)
}
}
2. Monadic Pipeline (Railway-Oriented)
Chain synchronous and asynchronous operations effortlessly with automatic failure short-circuiting:
public async Task<Result<OrderDto>> ProcessOrderAsync(
Guid userId,
CreateOrderCommand command,
CancellationToken cancellationToken)
{
return await _userService.GetUserAsync(userId, cancellationToken)
.Ensure(u => u.IsActive, UserErrors.Suspended, cancellationToken)
.Bind(u => _orderService.CreateOrderAsync(u, command, cancellationToken))
.TapOnSuccess(order => _logger.LogInformation("Order {Id} created", order.Id), cancellationToken)
.TapOnFailure(error => _logger.LogWarning("Order creation failed: {Code}", error.Code), cancellationToken)
.Map(order => new OrderDto(order.Id, order.TotalAmount), cancellationToken);
}
3. Zero-Allocation TState Pattern
Standard lambdas capturing local variables allocate compiler display closures on the heap. High-throughput paths can eliminate closure allocations by passing TState alongside static lambdas:
var minPrice = 50.0m;
var maxPrice = 500.0m;
// โ Allocates: Captures 'minPrice' and 'maxPrice' in a heap closure object
var result = GetProduct(id)
.Ensure(p => p.Price >= minPrice && p.Price <= maxPrice,
Error.Validation("Price.OutOfRange", "Price out of bounds"));
// โ
Zero Closure Allocation: State passed via tuple and evaluated in static lambda
var state = (minPrice, maxPrice);
var result = GetProduct(id)
.Ensure(state,
static (s, p) => p.Price >= s.minPrice && p.Price <= s.maxPrice,
Error.Validation("Price.OutOfRange", "Price out of bounds"));
TState overloads are available for all monadic operators: Map, Bind, TapOnSuccess, TapOnFailure, Match, Execute, Ensure, and Recover.
4. Pattern Matching & Safe Unwrapping
Safely consume results using functional matching, idiomatic TryGetValue, or tuple destructuring:
// 1. Functional Pattern Matching
string message = result.Match(
dto => $"Success: Order {dto.Id} processed with total ${dto.TotalAmount}",
error => $"Error [{error.Code}]: {error.Description}"
);
// 2. Idiomatic TryGetValue Pattern
if (result.TryGetValue(out var orderDto))
{
Console.WriteLine($"Order ID: {orderDto.Id}");
}
// 3. Tuple Destructuring
var (isSuccess, value, error) = result;
if (isSuccess)
{
Console.WriteLine($"Processed: {value.Id}");
}
else
{
Console.WriteLine($"Failed: {error.Description}");
}
5. Compound Validation
Aggregate multiple validation errors concurrently without throwing or allocating unnecessary intermediate collections:
// Declarative rule evaluation with Result.ValidateAll
public Result<Order> ValidateOrder(Order order)
{
return Result.ValidateAll(
order,
static o => o.Items.Count > 0
? Result.Success()
: Error.Validation("Order.NoItems", "Order must contain at least one item."),
static o => o.TotalAmount > 0
? Result.Success()
: Error.Validation("Order.InvalidAmount", "Total amount must be greater than zero.")
);
}
// Imperative accumulation with stack-allocated ErrorBuilder
public Result<Customer> CreateCustomer(CreateCustomerCommand command)
{
var builder = ErrorBuilder.Validation("Customer.InvalidPayload", "Customer validation failed.");
if (string.IsNullOrWhiteSpace(command.Name))
builder.WithInnerError(Error.Validation("Customer.NameRequired", "Name is required."));
if (command.Age < 18)
builder.WithInnerError(Error.Validation("Customer.Underage", "Customer must be at least 18 years old."));
if (builder.HasInnerErrors)
return builder.Build();
return new Customer(command.Name, command.Age);
}
๐ก Core Use Cases
Use Case 1: Clean Architecture Application Services / CQRS
Encapsulate application business workflows where command and query handlers return domain outcomes with explicit status codes:
public class ConfirmPaymentCommandHandler : IRequestHandler<ConfirmPaymentCommand, Result<PaymentConfirmationDto>>
{
private readonly IPaymentGateway _gateway;
private readonly IOrderRepository _orders;
public ConfirmPaymentCommandHandler(IPaymentGateway gateway, IOrderRepository orders)
{
_gateway = gateway;
_orders = orders;
}
public async Task<Result<PaymentConfirmationDto>> Handle(
ConfirmPaymentCommand command,
CancellationToken cancellationToken)
{
return await _orders.GetByIdAsync(command.OrderId, cancellationToken)
.Ensure(o => o.Status == OrderStatus.PendingPayment,
Error.Conflict("Order.InvalidState", "Order is not pending payment."))
.Bind(o => _gateway.ChargeAsync(o, command.PaymentMethod, cancellationToken))
.Map(receipt => new PaymentConfirmationDto(receipt.TransactionId, receipt.AmountPaid));
}
}
Use Case 2: Multi-Step Domain Workflow with Short-Circuiting
Coordinate multiple operations where failure in any intermediate step immediately aborts downstream execution without extra indentation:
public async Task<Result<SubscriptionDto>> ActivateSubscriptionAsync(
Guid accountId,
string planCode,
CancellationToken ct)
{
return await _accountService.GetAccountAsync(accountId, ct)
.Ensure(a => a.IsVerified, Error.Forbidden("Account.Unverified", "Account email must be verified."), ct)
.Bind(a => _billingService.ValidatePaymentMethodAsync(a.Id, ct), ct)
.Bind(_ => _planCatalog.GetPlanByCodeAsync(planCode, ct), ct)
.Bind(plan => _subscriptionService.ProvisionAsync(accountId, plan, ct), ct)
.TapOnSuccess(sub => _eventBus.PublishAsync(new SubscriptionActivatedEvent(sub.Id), ct), ct)
.Map(sub => new SubscriptionDto(sub.Id, sub.ExpiresAt), ct);
}
Use Case 3: Compound Form & Entity Validation
Collect all field validation issues simultaneously and return a single aggregated ProblemDetails response:
public Result<UserProfile> UpdateProfile(UpdateProfileCommand command)
{
return Result.ValidateAll(
command,
static c => !string.IsNullOrWhiteSpace(c.DisplayName)
? Result.Success()
: Error.Validation("Profile.DisplayNameRequired", "Display name is required."),
static c => c.BirthDate < DateOnly.FromDateTime(DateTime.UtcNow)
? Result.Success()
: Error.Validation("Profile.InvalidBirthDate", "Birth date must be in the past."),
static c => c.Bio?.Length <= 500
? Result.Success()
: Error.Validation("Profile.BioTooLong", "Bio cannot exceed 500 characters.")
).Map(c => new UserProfile(c.DisplayName, c.BirthDate, c.Bio));
}
Use Case 4: Repository Querying with Maybe<T>
Express optional domain entities without relying on null references or nullable annotations:
using EricksonLopez.Result.Maybe;
public class CustomerRepository : ICustomerRepository
{
public async Task<Maybe<Customer>> FindByTaxIdAsync(string taxId, CancellationToken ct)
{
Customer? entity = await _dbContext.Customers.FirstOrDefaultAsync(c => c.TaxId == taxId, ct);
return Maybe.From(entity);
}
}
// Seamless conversion to Result when absence constitutes a domain failure:
Maybe<Customer> maybeCustomer = await _customerRepository.FindByTaxIdAsync("TAX-12345", ct);
Result<Customer> result = maybeCustomer.ToResult(Error.NotFound("Customer.NotFound", "Customer not found."));
Use Case 5: Compile-Time Strongly-Typed Domain Error Hierarchies
Enforce exhaustive compile-time error handling with Result<TValue, TError>:
using EricksonLopez.Result.Generic;
public abstract record OrderProcessingError;
public sealed record InventoryUnavailable(string Sku) : OrderProcessingError;
public sealed record CardDeclined(string Reason) : OrderProcessingError;
public Result<Order, OrderProcessingError> PlaceOrder(OrderRequest request)
{
if (!_inventory.HasStock(request.Sku))
return new InventoryUnavailable(request.Sku);
if (!_payment.Charge(request.Payment))
return new CardDeclined("Insufficient balance");
return new Order(request.Sku, request.Quantity);
}
Use Case 6: Async Compound Validation with ValidateAllAsync
When validation rules require async I/O (e.g., database lookups, external API calls), use Result.ValidateAllAsync to evaluate all rules sequentially and aggregate all failures into a single compound error:
public async Task<Result<Order>> CreateOrderAsync(CreateOrderCommand command, CancellationToken ct)
{
// Evaluates ALL validators โ does not short-circuit on first failure.
// Passes cancellationToken to each validator individually.
Result validationResult = await Result.ValidateAllAsync(
new List<Func<CancellationToken, Task<Result>>>
{
async token => await _customerRepo.ExistsAsync(command.CustomerId, token)
? Result.Success()
: Error.NotFound("Customer.NotFound", $"Customer '{command.CustomerId}' not found."),
async token => await _inventoryService.HasStockAsync(command.Sku, command.Quantity, token)
? Result.Success()
: Error.Conflict("Order.InsufficientStock", $"Insufficient stock for SKU '{command.Sku}'."),
async token => await _paymentService.ValidateMethodAsync(command.PaymentMethodId, token)
? Result.Success()
: Error.Validation("Payment.InvalidMethod", "Payment method is invalid or expired.")
},
cancellationToken: ct);
if (validationResult.IsFailure)
return validationResult.Error; // compound error with all InnerErrors populated
return await _orderService.CreateAsync(command, ct);
}
ValueTaskoverload: UseValidateAllAsync(IReadOnlyList<Func<CancellationToken, ValueTask<Result>>>, CancellationToken)when validators returnValueTask<Result>to avoid unnecessary task boxing overhead.Value-preserving overloads:
ValidateAllAsync<T>(T value, validators, ct)validates the value against all rules and returnsTask<Result<T>>(orValueTask<Result<T>>), preserving the value if all validators pass.
๐ Configuration & Integrations
ASP.NET Core & RFC 9457 ProblemDetails
EricksonLopez.Result.AspNetCore maps Result and Result<T> directly to HTTP responses according to RFC 9457 (ProblemDetails).
using EricksonLopez.Result.AspNetCore;
using EricksonLopez.Result.OpenApi;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Direct ToHttpResult() (Zero boxing, full OpenAPI metadata)
app.MapGet("/users/{id:guid}", (Guid id, UserService userService) =>
{
return userService.GetUser(id).ToHttpResult();
});
// Automatic Minimal API Endpoint Filter unwrapping Result responses
app.MapPost("/orders", (CreateOrderCommand command, OrderService orderService) =>
{
return orderService.CreateOrder(command); // Returns Result<OrderDto>
})
.AddResultEndpointFilter()
.ProducesResult<OrderDto>(StatusCodes.Status200OK);
app.Run();
Security Configuration for Problem Details
Configure environment guards to avoid leaking internal exception messages in production:
builder.Services.Configure<ResultHttpOptions>(options =>
{
// Descriptions are included only in Development environments (Enforced by RESULT009)
options.IncludeDescription = builder.Environment.IsDevelopment();
options.DefaultInstanceUri = "/errors";
});
OpenAPI Metadata Extensions
EricksonLopez.Result.OpenApi provides metadata extensions to document all possible HTTP status codes and response schemas:
app.MapPost("/api/v1/payments", (PaymentRequest request, IPaymentService svc) => svc.Pay(request))
.AddResultEndpointFilter()
.ProducesResult<PaymentReceiptDto>(StatusCodes.Status200OK)
.ProducesProblemDetails(StatusCodes.Status400BadRequest)
.ProducesProblemDetails(StatusCodes.Status401Unauthorized)
.ProducesProblemDetails(StatusCodes.Status409Conflict)
.ProducesProblemDetails(StatusCodes.Status500InternalServerError);
OpenTelemetry Tracing & Metrics
EricksonLopez.Result.OpenTelemetry automatically enriches active OpenTelemetry Activity spans and tracks real-time BCL metrics counters:
using EricksonLopez.Result.OpenTelemetry;
public async Task<Result<PaymentReceipt>> ExecutePaymentAsync(PaymentRequest request)
{
using var activity = MyActivitySource.StartActivity("ExecutePayment");
var result = await _paymentGateway.ProcessAsync(request);
// Records error status, error code, type, severity, and metadata tags on the Activity span
result.TraceOutcome("ExecutePayment", activity);
// Increment metrics counter (BCL System.Diagnostics.Metrics)
ResultMetrics.StaticTrackSuccess("ExecutePayment");
return result;
}
JSON Serialization & NativeAOT
EricksonLopez.Result.Serialization provides trim-safe System.Text.Json custom converters for Result, Result<T>, Maybe<T>, and Error.
using System.Text.Json;
using EricksonLopez.Result.Serialization;
var options = new JsonSerializerOptions();
options.Converters.Add(new ResultJsonConverter());
options.Converters.Add(new ResultOfTJsonConverter<OrderDto>());
options.Converters.Add(new ErrorJsonConverter());
// NativeAOT JSON Source Generation setup:
[JsonSerializable(typeof(Result<OrderDto>))]
[JsonSerializable(typeof(Error))]
internal partial class AppJsonSerializerContext : JsonSerializerContext { }
FluentValidation Integration
EricksonLopez.Result.FluentValidation maps FluentValidation.Results.ValidationResult directly into structured Result failures:
using EricksonLopez.Result.FluentValidation;
using FluentValidation;
public class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
{
public CreateUserCommandValidator()
{
RuleFor(x => x.Email).NotEmpty().EmailAddress();
RuleFor(x => x.Age).GreaterThanOrEqualTo(18);
}
}
// Execution:
ValidationResult validationResult = await validator.ValidateAsync(command, ct);
Result result = validationResult.ToValidationResult();
MediatR Pipeline Behavior
EricksonLopez.Result.MediatR provides a pipeline behavior that intercepts unhandled exceptions in MediatR command and query handlers, wrapping them as structured Result failures:
using EricksonLopez.Result.MediatR;
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
builder.Services.AddResultExceptionBehavior();
Roslyn Diagnostic Analyzers
The EricksonLopez.Result.Analyzers package is bundled with the core EricksonLopez.Result package and enforces correctness and performance invariants at compile time:
| Diagnostic ID | Severity | Category | Description | CodeFix |
|---|---|---|---|---|
RESULT001 |
โ ๏ธ Warning | Performance | Result<T> struct size exceeds 32 bytes โ recommends using a class |
โ |
RESULT003 |
๐ Error | Usage | ErrorBuilder.With*() return value is discarded (mutation lost) |
โ Assign Return |
RESULT004 |
โ ๏ธ Warning | Performance | Lambda captures outer variable in pipeline (closure allocation) | โ
Use TState |
RESULT005 |
โ ๏ธ Warning | Performance | Error.WithMetadata() chained 3+ times consecutively without batching |
โ |
RESULT006 |
โ ๏ธ Warning | Performance | ErrorBuilder.WithInnerError() chained 2+ times consecutively |
โ |
RESULT007 |
โ ๏ธ Warning | Reliability | HashSet<Error> or LINQ deduplication used without ErrorEqualityComparer.Strict |
โ |
RESULT008 |
โ ๏ธ Warning | Usage | AddResultEndpointFilter() used without explicit .Produces<T>() metadata |
โ |
RESULT009 |
โ ๏ธ Warning | Security | ResultHttpOptions.IncludeDescription = true set without environment guard |
โ |
RESULT010 |
โ ๏ธ Warning | Security | Exception.Message passed directly to ResultExceptionBehavior error factory |
โ |
RESULT012 |
โ ๏ธ Warning | Usage | Method returns default(Result) or default(Result<T>) |
โ |
RESULT_OTEL_001 |
โน๏ธ Info | Observability | TraceOutcome() called without ResultMetrics registered |
โ |
RESULT_GEN_001 |
โ ๏ธ Warning | Usage | [JsonSerializable(typeof(Result))] has no effect for converter generation |
โ |
๐งช Testing & Quality
Fluent Assertions API
EricksonLopez.Result.Testing simplifies unit test assertions with a declarative, chainable API:
using EricksonLopez.Result.Testing;
using Xunit;
public class UserServiceTests
{
[Fact]
public void GetUser_ShouldReturnSuccess_WhenUserExists()
{
Result<User> result = _userService.GetUser(existingId);
result.ShouldBeSuccess()
.Value.Name.ShouldBe("Erickson");
}
[Fact]
public async Task GetUserAsync_ShouldReturnFailure_WhenNotFound()
{
Result<User> result = await _userService.GetUserAsync(nonExistingId);
result.ShouldBeFailure()
.ShouldHaveErrorCode("User.NotFound")
.ShouldHaveErrorType(ErrorType.NotFound);
}
}
Test Framework Adapters
When testing with specific frameworks, test failure assertions surface using native exception types:
# xUnit: Failures throw Xunit.Sdk.XunitException
dotnet add package EricksonLopez.Result.Testing.XUnit
# NUnit: Failures throw NUnit.Framework.AssertionException
dotnet add package EricksonLopez.Result.Testing.NUnit
ValueTask & Async Deadlock Avoidance
The testing suite avoids standard Task.Result or ValueTask.GetAwaiter().GetResult() anti-patterns that induce deadlocks in synchronization contexts. All assertions fully support asynchronous evaluation:
await resultTask.ShouldBeSuccessAsync();
await resultTask.ShouldBeFailureAsync();
Mutation Testing & Quality Gates
The test suite enforces rigorous quality guarantees verified across CI/CD:
- 100.00% Line Coverage and 100.00% Method Coverage across all core modules.
- Stryker.NET Mutation Testing with a certified 100.00% Score on Core and โฅ98% Global Score (
break: 95). - Zero surviving mutants across all monadic combinators, validation aggregators, and struct state machines.
โก Performance Benchmarks
Environment: .NET 10.0.10, X64 RyuJIT AVX-512, BenchmarkDotNet v0.15.8
Result Construction Benchmark
| Method | Mean | Allocated |
|---|---|---|
Result.Success() |
0.000 ns | 0 B |
Result.Success("value") |
0.000 ns | 0 B |
Result.Success(42) |
0.003 ns | 0 B |
Implicit TValue โ Result<T> |
0.006 ns | 0 B |
Implicit Error โ Result<T> |
0.757 ns | 0 B |
Result.Failure(error) (non-generic) |
0.757 ns | 0 B |
Result.Failure(error) (generic int) |
0.782 ns | 0 B |
Result.Success(Guid.NewGuid()) |
35.437 ns | 0 B |
โ ๏ธ
Success(Guid.NewGuid())latency comes fromGuid.NewGuid()itself (OS entropy), not from theResult<T>wrapper. The wrapper always allocates 0 B regardless of value type.
Pipeline Operations Benchmark
| Method | Mean | Allocated |
|---|---|---|
TapOnSuccess (success, lambda) |
0.16 ns | 0 B |
Map (success, lambda) |
0.77 ns | 0 B |
Ensure (success, passes) |
0.94 ns | 0 B |
Bind (success, lambda) |
2.12 ns | 0 B |
Full pipeline (3-stage, TState) |
7.17 ns | 32 B |
โ ๏ธ Pipeline benchmark numbers above are pre-release estimates. Committed benchmark results for
ResultPipelineBenchmarksare not yet inbenchmarks/results/results/. Actual measurements may differ.
๐ Compatibility & Technical Matrix
Framework & NativeAOT Support
| Package | .NET 8.0 LTS | .NET 9.0 STS | .NET 10.0 | NativeAOT | Trimmable | Notes |
|---|---|---|---|---|---|---|
EricksonLopez.Result |
โ | โ | โ | โ Certified | โ Certified | Zero reflection in core types |
EricksonLopez.Result.Generic |
โ | โ | โ | โ Certified | โ Certified | Struct layout with zero reflection |
EricksonLopez.Result.Maybe |
โ | โ | โ | โ Certified | โ Certified | Struct layout with zero reflection |
EricksonLopez.Result.AspNetCore |
โ | โ | โ | โ Certified | โ Certified | STJ source generator compatible |
EricksonLopez.Result.OpenApi |
โ | โ | โ | โ Certified | โ Certified | Minimal API OpenAPI metadata |
EricksonLopez.Result.OpenTelemetry |
โ | โ | โ | โ Certified | โ Certified | Native BCL Activity & Metrics |
EricksonLopez.Result.Serialization |
โ | โ | โ | โ ๏ธ Partial | โ ๏ธ Partial | Use explicit ResultOfTJsonConverter<T> for AOT |
EricksonLopez.Result.Serialization.Generators |
netstandard2.0 |
netstandard2.0 |
netstandard2.0 |
โ Tool | โ Tool | Roslyn Source Generator (compile time) |
EricksonLopez.Result.FluentValidation |
โ | โ | โ | โ Certified | โ Certified | No reflection in mapping layer |
EricksonLopez.Result.MediatR |
โ | โ | โ | โ No | โ No | MediatR uses dynamic reflection (ADR-018) |
EricksonLopez.Result.Testing |
โ | โ | โ | โ Test Only | โ Test Only | Test assertions library |
EricksonLopez.Result.Testing.XUnit |
โ | โ | โ | โ Test Only | โ Test Only | xUnit test runner adapter |
EricksonLopez.Result.Testing.NUnit |
โ | โ | โ | โ Test Only | โ Test Only | NUnit test runner adapter |
EricksonLopez.Result.Analyzers |
netstandard2.0 |
netstandard2.0 |
netstandard2.0 |
โ Tool | โ Tool | Roslyn Analyzer (runs inside compiler) |
HTTP Status Code Mapping Matrix
ErrorType |
HTTP Status Code | RFC 9457 Title | Typical Domain Scenario |
|---|---|---|---|
Validation |
400 Bad Request | Bad Request | Input validation failure, invariant violation |
Unauthorized |
401 Unauthorized | Unauthorized | Missing or expired authentication token |
Forbidden |
403 Forbidden | Forbidden | Insufficient permissions for requested resource |
NotFound |
404 Not Found | Not Found | Entity or aggregate root does not exist |
Conflict |
409 Conflict | Conflict | Concurrency conflict, duplicate unique key |
Unavailable |
503 Service Unavailable | Service Unavailable | Downstream dependency or circuit breaker tripped |
Failure / Unexpected |
500 Internal Server Error | Internal Server Error | Unhandled domain failure or unexpected exception |
๐๏ธ Architecture & Design Principles
Railway-Oriented Programming (ROP) Flow
flowchart TD
Start[Input Data] --> Ensure{Ensure(predicate)}
Ensure -- Pass --> Bind[Bind(Async Service Call)]
Ensure -- Fail --> FailTrack[Return Result.Failure]
Bind -- Success --> Map[Map(To DTO)]
Bind -- Failure --> FailTrack
Map --> Tap[Tap(Side Effects / Logging)]
Tap --> Match{Match / ToHttpResult}
Match -- Success --> OkRes[200 OK / Success Value]
Match -- Failure --> ProbRes[RFC 9457 ProblemDetails]
style FailTrack fill:#ff9999,stroke:#333,stroke-width:2px
style OkRes fill:#99ccff,stroke:#333,stroke-width:2px
style ProbRes fill:#ffcc99,stroke:#333,stroke-width:2px
Struct Memory Layout & State Lifecycle
Result and Result<TValue> use a 3-state discriminant (Uninitialized = 0, Success = 1, Failure = 2) guaranteeing zero default initialization bugs:
stateDiagram-v8
[*] --> Uninitialized: default(Result) / default(Result<T>)
Uninitialized --> Success: Result.Success() / Result.Success(value)
Uninitialized --> Failure: Result.Failure(error)
state Success {
IsSuccess: true
IsFailure: false
Value: Valid Instance
Error: Throws InvalidOperationException
}
state Failure {
IsSuccess: false
IsFailure: true
Value: Throws InvalidOperationException
Error: Valid Error Instance
}
state Uninitialized {
IsUninitialized: true
Value: Throws InvalidOperationException
Error: Returns WellKnownErrors.UninitializedError (sentinel, does NOT throw)
}
๐ก๏ธ Best Practices & Anti-Patterns
Recommended vs Avoid
| Scenario | โ Avoid | โ Recommended |
|---|---|---|
| Domain Failures | Throwing NotFoundException or ValidationException |
Returning Result<T> with semantic Error.NotFound() |
| High-Throughput Pipelines | Lambda capturing outer variables (.Ensure(x => x > limit)) |
Passing TState via static lambda (.Ensure(limit, static (l, x) => x > l)) |
| Unwrapping Values | Accessing .Value directly without checking IsSuccess |
Using .Match(), .TryGetValue(), or tuple destructuring |
| Uninitialized Structs | Returning default(Result) or default(Result<T>) |
Returning Result.Success(...) or Result.Failure(...) (Enforced by RESULT012) |
| Error Collections | new HashSet<Error>() with default equality |
new HashSet<Error>(ErrorEqualityComparer.Strict) (Enforced by RESULT007) |
| Minimal APIs | Using AddResultEndpointFilter() without .Produces<T>() |
Calling .ToHttpResult() or pairing with .ProducesResult<T>() (Enforced by RESULT008) |
| Compound Errors | Chaining .WithMetadata() 3+ times consecutively |
Using ErrorBuilder or passing a dictionary (Enforced by RESULT005) |
โ ๏ธ Troubleshooting & Common Pitfalls
Review the following common pitfalls that can produce unexpected runtime behavior:
1. default(Result) evaluates silently as false in boolean context
Result and Result<T> are structs. An uninitialized struct (default) has ResultState.Uninitialized. Both IsSuccess and IsFailure evaluate to false. Always check res.IsUninitialized or avoid returning default structs (enforced by Roslyn analyzer RESULT012).
2. AddResultEndpointFilter() requires .Produces<T>() or .ProducesResult<T>()
AddResultEndpointFilter() unwraps results dynamically via IResultOutcome. To ensure Swagger/OpenAPI generators infer the proper response DTO schema instead of object, always chain .ProducesResult<T>() (enforced by RESULT008).
3. HashSet<Error> deduplicates errors with identical semantic fields
By default, Error.Equals() compares only the 5 domain fields (Code, Description, Type, Severity, Retryability). If collections must distinguish errors by TraceId, CorrelationId, or Metadata, explicitly provide ErrorEqualityComparer.Strict (enforced by RESULT007).
4. ErrorBuilder mutation discarded when return value is not assigned
ErrorBuilder is a stack-allocated readonly struct. Calling .WithInnerError() or .WithMetadata() returns a mutated copy. Discarding the return value loses the added data (enforced as a compile error by RESULT003).
๐ Part of the EricksonLopez Ecosystem
EricksonLopez.Result is a foundational component of the EricksonLopez open-source library ecosystem:
- ๐งฑ EricksonLopez.SharedKernel โ Domain Primitives, Specifications, and Domain Events.
- โก EricksonLopez.Result โ High-Performance Struct-Based Result Pattern & Telemetry.
๐ค Contributing
We welcome contributions, bug reports, documentation improvements, and feature suggestions!
Development Setup
- Prerequisites: .NET 8.0 / 9.0 / 10.0 SDK, Git, and an IDE (Rider, Visual Studio 2022+, or VS Code).
- Build Solution:
dotnet build EricksonLopez.Result.slnx - Run All Tests:
dotnet test EricksonLopez.Result.slnx --configuration Release - Run Mutation Tests:
cd tests/EricksonLopez.Result.Tests dotnet stryker
Please read our Contributing Guide and Code of Conduct before submitting pull requests.
๐ License
Distributed under the MIT License. Copyright ยฉ 2026 Erickson Lopez.
Learn more about Target Frameworks and .NET Standard.
This package has 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.