EricksonLopez.Result.Testing 1.0.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package EricksonLopez.Result.Testing --version 1.0.0
                    
NuGet\Install-Package EricksonLopez.Result.Testing -Version 1.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="EricksonLopez.Result.Testing" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="EricksonLopez.Result.Testing" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="EricksonLopez.Result.Testing" />
                    
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 EricksonLopez.Result.Testing --version 1.0.0
                    
#r "nuget: EricksonLopez.Result.Testing, 1.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 EricksonLopez.Result.Testing@1.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=EricksonLopez.Result.Testing&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=EricksonLopez.Result.Testing&version=1.0.0
                    
Install as a Cake Tool

EricksonLopez.Result

NuGet NuGet Downloads CI Coverage Mutation Score License: MIT .NET NativeAOT

A high-performance, struct-based, enterprise-grade Result Pattern ecosystem for modern .NET (.NET 8, .NET 9, .NET 10). Designed for production systems demanding zero allocation on happy paths, rich domain error taxonomy, RFC 9457 HTTP ProblemDetails integration, distributed OpenTelemetry tracing, System.Text.Json source generation, FluentValidation integration, MediatR pipeline behaviors, Roslyn analyzers, and fluent testing assertions.


๐Ÿ“ฆ Ecosystem Packages

Package Version Description
EricksonLopez.Result NuGet Core struct-based Result, Error domain model, monadic pipeline, LINQ support, and bundled Roslyn analyzers
EricksonLopez.Result.AspNetCore NuGet ASP.NET Core Minimal APIs filter & RFC 9457 ProblemDetails HTTP response mapper
EricksonLopez.Result.OpenTelemetry NuGet OpenTelemetry ActivitySource tracing integration and System.Diagnostics.Metrics counters
EricksonLopez.Result.Serialization NuGet System.Text.Json custom converters and NativeAOT trim-safe JsonSerializerContext
EricksonLopez.Result.Serialization.Generators NuGet Roslyn source generator for AOT-compatible Result<T> JSON serialization
EricksonLopez.Result.FluentValidation NuGet FluentValidation integration โ€” converts ValidationResult to structured Result failures
EricksonLopez.Result.MediatR NuGet MediatR pipeline behavior โ€” catches unhandled exceptions and wraps them as Result failures
EricksonLopez.Result.Testing NuGet Framework-agnostic fluent testing assertion library (ShouldBeSuccess(), ShouldHaveError())
EricksonLopez.Result.Testing.XUnit NuGet xUnit-specific test helpers โ€” assertion failures surface as XunitException
EricksonLopez.Result.Testing.NUnit NuGet NUnit-specific test helpers โ€” assertion failures surface as AssertionException
EricksonLopez.Result.Analyzers NuGet Roslyn analyzers & code fixes (RESULT001โ€“009) โ€” bundled with Core, also available standalone

โšก Key Features

  • ๐Ÿš€ Zero-Allocation Envelope: Core Result and Result<TValue> are readonly struct value types โ€” zero heap allocation for success results.
  • โšก Closure-Free TState Pipeline: All monadic operators (Map, Bind, Tap, Match, Switch, Ensure, Recover) offer TState overloads to completely eliminate lambda closure allocations in hot execution paths.
  • ๐Ÿ”’ Rich Enterprise Error Taxonomy: Extensible Error class featuring ErrorType, ErrorSeverity, ErrorRetryability, lazy TraceId (ambient Activity), CorrelationId, localized keys, and immutable Metadata.
  • ๐Ÿงฉ Span-Based Result.Combine: Aggregates up to 8 typed tuples or ReadOnlySpan<Result> using ArrayPool<Error> to eliminate temporary array allocations.
  • ๐ŸŒ ASP.NET Core RFC 9457 Integration: Automatic mapping of Result to HTTP responses (200 OK, 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict, 503 Unavailable, 500 Server Error).
  • ๐Ÿ“Š First-Class OpenTelemetry: Automatically attaches error status, code, type, severity, and metadata to active Activity spans and records metrics via System.Diagnostics.Metrics.
  • โšก NativeAOT & Trimming Safe: Designed with zero reflection in hot paths, featuring source-generated JsonSerializerContext definitions.
  • ๐Ÿงช Fluent Test Assertions: Declarative assertion API for unit testing with standard test frameworks (xUnit, NUnit, MSTest). Fully supports asynchronous ValueTask validation avoiding testing framework deadlocks.

Table of Contents


Installation

Install the required packages using the .NET CLI:

# Core Package (includes bundled Roslyn analyzers)
dotnet add package EricksonLopez.Result

# Optional Framework & Tooling Packages
dotnet add package EricksonLopez.Result.AspNetCore
dotnet add package EricksonLopez.Result.OpenTelemetry
dotnet add package EricksonLopez.Result.Serialization
dotnet add package EricksonLopez.Result.FluentValidation
dotnet add package EricksonLopez.Result.MediatR
dotnet add package EricksonLopez.Result.Testing
dotnet add package EricksonLopez.Result.Testing.XUnit  # For xUnit test projects
dotnet add package EricksonLopez.Result.Testing.NUnit  # For NUnit test projects

Quick Start

1. Core Result & Domain Errors

Return Result or Result<T> instead of throwing control-flow 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
{
    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 without try/catch or nested if/else checks:

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))
        .Tap(order => _logger.LogInformation("Order {Id} created", order.Id), cancellationToken)
        .TapError(error => _logger.LogWarning("Order creation failed: {Code}", error.Code), cancellationToken)
        .Map(order => new OrderDto(order.Id, order.TotalAmount), cancellationToken);
}

Pattern Matching & Unwrapping:

// Fluent pattern matching
string response = result.Match(
    dto => $"Success: Order {dto.Id}",
    error => $"Error ({error.Code}): {error.Description}"
);

// Try-Get pattern (Idiomatic .NET)
if (result.TryGetValue(out var order))
{
    Console.WriteLine(order.Id);
}

// Tuple Destructuring
var (isSuccess, value, error) = result;
if (isSuccess)
{
    Console.WriteLine(value.Id);
}

3. Zero-Allocation TState Pattern

Standard lambda expressions capturing local variables create heap-allocated closure objects. High-throughput applications can eliminate closure allocations by passing TState as a parameter alongside static lambdas:

var minPrice = 50.0m;
var maxPrice = 500.0m;

// โŒ Allocates: captures 'minPrice' and 'maxPrice' in a 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 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"));

Overloads supporting TState are available for Map, Bind, Tap, Match, Switch, Ensure, and Recover.


4. ASP.NET Core Integration

EricksonLopez.Result.AspNetCore maps Result and Result<T> directly to HTTP responses according to RFC 9457 (ProblemDetails).

using EricksonLopez.Result.AspNetCore;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Minimal API Endpoint returning Result<T> converted to HttpResult
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()
.Produces<OrderDto>(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status400BadRequest);

app.Run();

HTTP Status Mapping Matrix:

ErrorType HTTP Status Code RFC Title
ErrorType.Validation 400 Bad Request Bad Request
ErrorType.Unauthorized 401 Unauthorized Unauthorized
ErrorType.Forbidden 403 Forbidden Forbidden
ErrorType.NotFound 404 Not Found Not Found
ErrorType.Conflict 409 Conflict Conflict
ErrorType.Unavailable 503 Service Unavailable Service Unavailable
ErrorType.Failure / Unexpected 500 Internal Server Error Internal Server Error

OpenAPI / Swagger Note: When using AddResultEndpointFilter(), the success value is returned as object? internally. OpenAPI metadata generators (e.g., Swashbuckle, NSwag) cannot automatically infer the response schema. You must add .Produces<T>(200) to your endpoint for accurate Swagger documentation, as shown in the example above. We provide a Roslyn Analyzer (RESULT007) that will warn you if you forget to add .Produces<T>().

Performance Note (Boxing): ResultEndpointFilter matches Result<T> via is IResultOutcome, which boxes the struct on every request (allocates on the heap regardless of success or failure). For high-throughput endpoints where zero allocation is critical, call ToHttpResult() directly from your handler instead:

app.MapGet("/orders/{id}", async (Guid id, IOrderService svc) =>
{
    Result<OrderDto> result = await svc.GetOrderAsync(id);
    return result.ToHttpResult(); // no boxing โ€” returns typed Ok<OrderDto>
});

This returns Ok<OrderDto> (not Ok<object?>) and gives OpenAPI tooling full type inference without .Produces<T>().


5. Distributed Tracing & Metrics

EricksonLopez.Result.OpenTelemetry automatically instruments active OpenTelemetry Activity spans and collects system metrics:

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
    activity?.RecordResult(result);
    
    // Instrument metrics counter and duration histogram
    ResultMetrics.RecordOutcome("ExecutePayment", result);

    return result;
}

6. JSON Serialization

EricksonLopez.Result.Serialization provides custom converters for Result, Result<T>, and Error.

using System.Text.Json;
using EricksonLopez.Result.Serialization;

var options = new JsonSerializerOptions();
options.Converters.Add(new ResultJsonConverterFactory());
options.Converters.Add(new ErrorJsonConverter());

// Serialize
string json = JsonSerializer.Serialize(Result.Success(42), options);
NativeAOT / Trimming Setup

โš ๏ธ Important: ResultJsonConverterFactory uses MakeGenericType and Activator.CreateInstance internally, which are not compatible with NativeAOT or aggressive trimming. For NativeAOT scenarios, you must register converters explicitly for each Result<T> you serialize.

Step 1: Register concrete converters for each Result<T> type:

using System.Text.Json;
using System.Text.Json.Serialization;
using EricksonLopez.Result.Serialization;

// โœ… NativeAOT-safe: register concrete converters directly
var options = new JsonSerializerOptions();
options.Converters.Add(new ResultJsonConverter());              // non-generic Result
options.Converters.Add(new ErrorJsonConverter());               // Error
options.Converters.Add(new ResultOfTJsonConverter<OrderDto>()); // each Result<T> you serialize
options.Converters.Add(new ResultOfTJsonConverter<int>());
options.Converters.Add(new ResultOfTJsonConverter<UserDto>());

Step 2: Register your DTOs in a JsonSerializerContext for source generation:

// Ensure ALL types used as T in Result<T> are registered for source generation.
// This is required for NativeAOT to generate the serialization metadata at compile time.
[JsonSerializable(typeof(OrderDto))]
[JsonSerializable(typeof(UserDto))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(Result))]
[JsonSerializable(typeof(Error))]
public partial class AppJsonContext : JsonSerializerContext { }

Step 3: Use in ASP.NET Core Minimal APIs (DI scenario):

builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default);
    options.SerializerOptions.Converters.Add(new ResultJsonConverter());
    options.SerializerOptions.Converters.Add(new ErrorJsonConverter());
    options.SerializerOptions.Converters.Add(new ResultOfTJsonConverter<OrderDto>());
    options.SerializerOptions.Converters.Add(new ResultOfTJsonConverter<UserDto>());
});

โŒ Do NOT use ResultJsonConverterFactory in NativeAOT builds โ€” it will fail at runtime with InvalidOperationException because MakeGenericType is not supported. Use the explicit per-type registration shown above.

โš ๏ธ Metadata Round-Trip Note: Error.Metadata values are serialized preserving native JSON types (numbers, booleans, strings). On deserialization, numeric types are recovered as long or double (not int, short, etc.), and DateTime/Guid/custom objects become string. This means Error.StrictEquals() will return false after a serialize โ†’ deserialize cycle if metadata contains numeric values (since int โ‰  long). Use Error.Equals() (which only compares Code, Description, Type, Severity) for post-serialization comparisons. For type-faithful round-tripping, store metadata as a typed DTO instead.

โš ๏ธ ASP.NET Core NativeAOT Note: The ResultEndpointFilter uses ErrorDetailDto in ProblemDetails extensions. If your app uses a JsonSerializerContext for NativeAOT, register ErrorDetailDto and List<ErrorDetailDto> in your context:

[JsonSerializable(typeof(EricksonLopez.Result.AspNetCore.ErrorDetailDto))]
[JsonSerializable(typeof(List<EricksonLopez.Result.AspNetCore.ErrorDetailDto>))]
public partial class AppJsonContext : JsonSerializerContext { }

7. Unit Testing with Fluent Assertions

EricksonLopez.Result.Testing simplifies unit test assertions and fully supports asynchronous execution without causing deadlocks in runners like Coverlet or xUnit:

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);

        await result.ShouldBeFailureAsync()
                    .ShouldHaveErrorAsync("User.NotFound")
                    .ShouldHaveErrorTypeAsync(ErrorType.NotFound);
    }
}

8. FluentValidation Integration

EricksonLopez.Result.FluentValidation converts FluentValidation.ValidationResult into structured Result failures with rich error metadata.

using EricksonLopez.Result.FluentValidation;

// Convert ValidationResult to Result
var validator = new OrderValidator();
Result result = validator.Validate(order).ToResult();

// Or validate and wrap the validated object in Result<T>
Result<Order> typedResult = validator.Validate(order).ToResult(order);

// Pipeline integration: validate inside a Result chain
var pipelineResult = await GetOrderAsync()
    .EnsureValid(new OrderValidator())
    .Map(order => ProcessOrder(order));

Each ValidationFailure is mapped to a structured Error with:

  • ErrorType.Validation type
  • Error code from ValidationFailure.ErrorCode (or Validation.{PropertyName} fallback)
  • Metadata: propertyName, attemptedValue, and FluentValidation severity mapping

9. MediatR Pipeline Behavior

EricksonLopez.Result.MediatR provides a pipeline behavior that catches unhandled exceptions in MediatR handlers and wraps them as Result failures.

using EricksonLopez.Result.MediatR;

// Register the behavior in DI
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
builder.Services.AddResultExceptionBehavior();

// Or with a custom error factory
builder.Services.AddResultExceptionBehavior(ex =>
    Error.Unexpected($"Handler.{ex.GetType().Name}", ex.Message)
         .WithMetadata("stackTrace", ex.StackTrace ?? ""));

Note: ResultExceptionBehavior only activates when the response type is Result or Result<T>. Non-Result responses pass through unmodified. OperationCanceledException is always re-thrown.


Roslyn Analyzers

The EricksonLopez.Result.Analyzers package is bundled with the core EricksonLopez.Result package and provides compile-time diagnostics:

Diagnostic ID Severity Description
RESULT001 โš ๏ธ Warning Result<T> used with a struct type larger than 64 bytes โ€” recommends using a class to avoid excessive copying

| RESULT003 | โš ๏ธ Warning | ErrorBuilder.With*() return value is discarded โ€” the mutated struct copy is lost |


API Reference

Comprehensive documentation for all primitives, pipeline methods, and packages:

  • ๐Ÿ“‘ Architecture Overview โ€” Mermaid diagrams, pipeline design, and component interaction.
  • ๐Ÿ”ง CI/CD & Build Pipeline โ€” GitHub Actions workflows, release strategy, and supply chain security.
  • ๐Ÿ“Š Quality Gates โ€” Code coverage, mutation testing, and static analysis configuration.
  • ๐Ÿ’ก Best Practices โ€” Recommended patterns for production applications.
  • โš ๏ธ Anti-Patterns โ€” Pitfalls, unsafe state accesses, and code smells to avoid.
  • ๐Ÿ“– Cookbook โ€” Copy-pasteable recipes for web APIs, OpenTelemetry, testing, and LINQ syntax.
  • ๐Ÿ”„ Migration Guide โ€” Guide for migrating from raw exceptions, FluentResults, CSharpFunctionalExtensions, OneOf, or ErrorOr.
  • โšก Allocation Analysis โ€” Deep dive into memory benchmarks, struct layout, and zero-allocation mechanics.
  • ๐Ÿงฌ Mutation Score โ€” Latest Stryker mutation testing results, threshold configuration (break: 95), and analysis of all surviving/equivalent mutants.
  • ๐Ÿ›๏ธ Architectural Decision Records (ADRs) โ€” ADRs 001 through 016 documenting key architectural choices.
  • ๐Ÿ“ฆ Package Reference โ€” Full compatibility matrix, dependency graph, and per-package details for all 11 NuGet packages.

Performance Benchmarks

Environment: .NET 10.0.10 (10.0.1026.32716), X64 RyuJIT AVX-512, BenchmarkDotNet v0.14.0

Result Construction โ€” Zero allocation on success

Method Mean Allocated
Result.Success() 0.000 ns 0 B
Result.Success("value") 0.000 ns 0 B
Result.Success(42) 0.002 ns 0 B
Implicit TValue โ†’ Result<T> 0.003 ns 0 B
Implicit Error โ†’ Result<T> 0.759 ns 0 B
Result.Failure(error) 0.777 ns 0 B

Pipeline Operations โ€” Sub-nanosecond Map/Bind

Method Mean Allocated
Tap (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

Combine โ€” ArrayPool-backed aggregation

Method Count Mean Allocated
All success 4 4.0 ns 0 B
One failure 4 4.3 ns 0 B
All success 64 26.3 ns 0 B
All failures 64 175.7 ns 696 B

Error Builder vs WithMetadata Chain

Method Mean Allocated
Error.Create(...).Build() 3.1 ns 96 B
Error.WithMetadata() ร— 3 chain 146.7 ns 752 B
ErrorBuilder.WithMetadata() ร— 3 85.8 ns 464 B

โšก Key insight: ErrorBuilder.WithMetadata() is 1.7ร— faster with 38% fewer allocations than chaining Error.WithMetadata() calls. Always use the builder for multiple metadata entries.

Async Pipeline โ€” Sync-path optimization

Method Mean Allocated
Map (sync completed) 7.9 ns 160 B
Map (async completed) 464.2 ns 237 B

๐Ÿ’ก When the Task<Result<T>> is already completed synchronously (common in cached/pooled scenarios), the async pipeline avoids the state machine entirely โ€” 59ร— faster than the true-async path.

Run benchmarks yourself:

cd benchmarks/EricksonLopez.Result.Benchmarks
dotnet run -c Release

NativeAOT & Trimming Compatibility

Package NativeAOT Compatible Trimmable Notes
EricksonLopez.Result โœ… Yes โœ… Yes Core types use zero reflection
EricksonLopez.Result.AspNetCore โœ… Yes โœ… Yes Compatible with STJ Source Generators
EricksonLopez.Result.OpenTelemetry โœ… Yes โœ… Yes Native OpenTelemetry Activity API
EricksonLopez.Result.Serialization โš ๏ธ Partial โš ๏ธ Partial ResultJsonConverterFactory uses MakeGenericType; use explicit ResultOfTJsonConverter<T> for AOT (see NativeAOT Setup)
EricksonLopez.Result.Serialization.Generators โœ… Yes โœ… Yes Source generator โ€” runs at compile time, dev dependency only
EricksonLopez.Result.FluentValidation โœ… Yes โœ… Yes No reflection in library code
EricksonLopez.Result.MediatR โŒ No โŒ No MediatR uses reflection internally; CreateFailure uses MakeGenericMethod
EricksonLopez.Result.Testing โœ… Yes โœ… Yes Unit test libraries are not compiled into AOT binaries

Common Pitfalls

The following patterns compile without errors but produce incorrect runtime behavior. Read these before shipping to production.

Pitfall 1 โ€” default(Result) evaluates silently as false in boolean context

Result and Result<T> are structs. When a field is not initialized (e.g., a not-null field in a class, or a struct obtained from new MyClass()), their default value is ResultState.Uninitialized โ€” which is neither success nor failure.

// Anti-pattern: default(Result) in a boolean context
Result result = default; // Uninitialized โ€” not Success, not Failure

if (result)              // evaluates as false โ€” looks like "failure" but is NOT
    Console.WriteLine("Success");
else
    Console.WriteLine("Failure");  // prints this, silently wrong

// The safe check:
if (result.IsUninitialized)
    throw new InvalidOperationException("Result was never assigned.");

Why this happens: operator true and operator false delegate to IsSuccess and IsFailure. An uninitialized result returns false for both, so if (result) silently evaluates to false.

Solution: Always obtain a Result through Result.Success() or Result.Failure(error), never through default or field initialization. If you receive a Result from a method that might return default, check IsUninitialized before using it.

// Safe: check for uninitialized state before using
var result = GetResultFromSomewhere();
if (result.IsUninitialized)
    throw new InvalidOperationException("Service returned an uninitialized result.");
return result.Match(/* ... */);

Pitfall 2 โ€” AddResultEndpointFilter() requires .Produces<T>() for OpenAPI

AddResultEndpointFilter() uses the IResultOutcome interface to detect Result<T> at runtime. This causes two side effects:

  1. Boxing on every request: Result<T> (a struct) is boxed to the heap per request (1โ€“2 allocations).
  2. OpenAPI schema degradation: The filter returns Ok<object?> internally. Swagger/NSwag cannot infer T โ€” the schema shows object without .Produces<T>().
// Anti-pattern: no .Produces<T>() โ€” OpenAPI shows object schema
app.MapGet("/orders/{id}", (Guid id) => orderService.GetOrder(id))
   .AddResultEndpointFilter(); // OpenAPI: responses: { "200": { schema: object } }

// Correct: explicit .Produces<T>() for full OpenAPI metadata
app.MapGet("/orders/{id}", (Guid id) => orderService.GetOrder(id))
   .AddResultEndpointFilter()
   .Produces<OrderDto>(StatusCodes.Status200OK)       // required for OpenAPI
   .ProducesProblem(StatusCodes.Status404NotFound);

For high-throughput endpoints (> 10k req/s), use ToHttpResult() directly to avoid boxing and get full OpenAPI inference automatically:

// Zero boxing + full OpenAPI inference โ€” preferred for performance-sensitive paths
app.MapGet("/orders/{id}", async (Guid id, IOrderService svc) =>
{
    var result = await svc.GetOrderAsync(id);
    return result.ToHttpResult(); // returns typed Ok<OrderDto>, no boxing
});

The Roslyn analyzer RESULT008 (EndpointFilterOpenApiAnalyzer) warns at compile time when .AddResultEndpointFilter() is called without .Produces<T>(). Install EricksonLopez.Result.Analyzers to enable it.

Pitfall 3 โ€” HashSet<Error> deduplicates errors with the same semantic fields

Error.Equals() compares 5 semantic fields: Code, Description, Type, Severity, Retryability. It intentionally excludes TraceId, CorrelationId, and Metadata (which vary per request).

// Two errors with same semantic fields but different trace IDs
var e1 = Error.NotFound("Order.NotFound", "Order not found").WithTraceId("trace-1");
var e2 = Error.NotFound("Order.NotFound", "Order not found").WithTraceId("trace-2");

var set = new HashSet<Error> { e1, e2 };
Console.WriteLine(set.Count); // 1 โ€” silently deduplicated!

// Safe: use ErrorEqualityComparer.Strict for strict equality (includes all fields)
var strictSet = new HashSet<Error>(ErrorEqualityComparer.Strict) { e1, e2 };
Console.WriteLine(strictSet.Count); // 2

The Roslyn analyzer RESULT007 (HashSetErrorEqualityAnalyzer) warns at compile time when new HashSet<Error>(), .Distinct(), .ToHashSet(), or .GroupBy() is used on Error sequences without ErrorEqualityComparer.Strict.



Part of the EricksonLopez Ecosystem

EricksonLopez.Result is a foundational component of the EricksonLopez library ecosystem:

  • ๐Ÿงฑ EricksonLopez.SharedKernel โ€” Domain Primitives, Specifications, and Domain Events.
  • โšก EricksonLopez.Result โ€” High-Performance Struct-Based Result Pattern & Telemetry.

License

Distributed under the MIT License. Copyright ยฉ 2026 Erickson Lopez.

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.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on EricksonLopez.Result.Testing:

Package Downloads
EricksonLopez.Result.Testing.XUnit

xUnit-specific testing helpers for EricksonLopez.Result. Provides ResultAssertionXUnitException which inherits from Xunit.Sdk.XunitException so failed assertions appear as "Failure" (not "Error") in xUnit's test output, producing cleaner assertion messages without unnecessary stack traces.

EricksonLopez.Result.Testing.NUnit

NUnit-specific testing helpers for EricksonLopez.Result. Provides ResultAssertionNUnitException which inherits from NUnit's AssertionException so failed assertions appear correctly in NUnit's test output.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.0 131 8/24/2026
1.0.0 158 8/1/2026