eQuantic.Core.Outcomes 2.0.0

dotnet add package eQuantic.Core.Outcomes --version 2.0.0
                    
NuGet\Install-Package eQuantic.Core.Outcomes -Version 2.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="eQuantic.Core.Outcomes" Version="2.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="eQuantic.Core.Outcomes" Version="2.0.0" />
                    
Directory.Packages.props
<PackageReference Include="eQuantic.Core.Outcomes" />
                    
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 eQuantic.Core.Outcomes --version 2.0.0
                    
#r "nuget: eQuantic.Core.Outcomes, 2.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 eQuantic.Core.Outcomes@2.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=eQuantic.Core.Outcomes&version=2.0.0
                    
Install as a Cake Addin
#tool nuget:?package=eQuantic.Core.Outcomes&version=2.0.0
                    
Install as a Cake Tool

eQuantic Core Outcomes Library

NuGet CI/CD License: MIT

A modern, type-safe Result Pattern implementation for .NET with Railway-Oriented Programming support. Handle success and failure cases elegantly without exceptions.

๐Ÿš€ What's New in v2.0

  • โœ… .NET 6 & .NET 8 support
  • โœ… Immutable records for thread-safety
  • โœ… Railway-Oriented Programming (Map, Bind, Match)
  • โœ… Typed error system with categorization
  • โœ… Full async/await support
  • โœ… ASP.NET Core integration with Problem Details (RFC 7807)
  • โœ… Result Combinators for aggregating multiple results
  • โœ… Observability & Tracing with correlation IDs and execution timing
  • โœ… Fluent API for better developer experience
  • โœ… Comprehensive test coverage

๐Ÿ“ฆ Installation

dotnet add package eQuantic.Core.Outcomes

Or via Package Manager:

Install-Package eQuantic.Core.Outcomes

๐Ÿ† Why Choose eQuantic.Core.Outcomes?

Unique Features (Not Available in Competing Libraries)

๐Ÿ” Built-in Observability & Distributed Tracing

  • โœ… CorrelationId & TraceId - Native OpenTelemetry, Jaeger, Zipkin integration
  • โœ… ExecutionTime - Automatic performance measurement with Timed()/TimedAsync()
  • โœ… Metadata - Rich contextual data for APM tools (Datadog, New Relic, Application Insights)
  • ๐Ÿ”ฅ UNIQUE: Only Result library with native observability support for microservices!

๐Ÿ”— Most Complete Result Combinators

  • โœ… Combine() - All-or-nothing aggregation
  • โœ… Zip() - Type-safe combination of 2-4 results
  • โœ… FirstSuccess() - Fallback/retry patterns
  • โœ… SuccessfulValues() - Graceful degradation
  • โœ… MergeErrors() - Comprehensive error collection
  • โœ… Partition() - Success/failure separation
  • ๐Ÿ”ฅ UNIQUE: 6 powerful combinators vs. 0-1 in other libraries!

โœจ Seamless FluentValidation Integration

  • โœ… Optional package: eQuantic.Core.Outcomes.FluentValidation
  • โœ… .ToResult() automatic conversion
  • โœ… .Validate() / .ValidateAsync() pipeline integration
  • ๐Ÿ”ฅ UNIQUE: Only library with native FluentValidation support!

๐ŸŒ Advanced ASP.NET Core Integration

  • โœ… Automatic RFC 7807 Problem Details conversion
  • โœ… Smart ErrorType โ†’ HTTP Status mapping
  • โœ… ToActionResult(), ToCreatedAtActionResult(), ToNoContentResult()
  • โœ… REST API best practices built-in

โšก Complete Async/Await Support

  • โœ… All operations: MapAsync, BindAsync, MatchAsync, TapAsync, EnsureAsync
  • โœ… Task unwrapping for cleaner pipelines
  • โœ… CancellationToken support throughout
  • โœ… Optimized with ConfigureAwait(false)

๐ŸŽจ Rich Typed Error System

// 8 semantic error types with factory methods
Error.Validation()    // Validation failures
Error.NotFound()      // Resource not found
Error.Conflict()      // Resource conflicts
Error.Unauthorized()  // Authentication failures
Error.Forbidden()     // Permission denials
Error.BusinessRule()  // Domain rule violations
Error.Technical()     // Infrastructure failures
Error.External()      // Third-party failures
Feature eQuantic.Outcomes FluentResults ErrorOr Ardalis.Result
Observability/APM โœ… Built-in โŒ None โŒ None โŒ None
Result Combinators โœ… 6 patterns โš ๏ธ 1 basic โŒ None โŒ None
FluentValidation โœ… Native โŒ Manual โŒ Manual โŒ Manual
RFC 7807 Auto โœ… Yes โš ๏ธ Basic โŒ No โš ๏ธ Partial
Async Support โœ… Complete โš ๏ธ Partial โŒ Limited โš ๏ธ Basic
Railway-Oriented โœ… Full โš ๏ธ Partial โš ๏ธ Partial โš ๏ธ Partial
Typed Errors โœ… 8 types โš ๏ธ Generic โš ๏ธ Basic โš ๏ธ Basic
.NET 6/8 โœ… Yes โœ… Yes โœ… Yes โœ… Yes
XML Docs โœ… Complete โš ๏ธ Basic โš ๏ธ Basic โš ๏ธ Basic

Our Advantage: We're the ONLY library combining observability, comprehensive combinators, FluentValidation integration, and full Railway-Oriented Programming in a modern, well-documented package.

๐ŸŽฏ Quick Start

Basic Usage

using eQuantic.Core.Outcomes;
using eQuantic.Core.Outcomes.Errors;

// Success case
var successResult = Result<int>.Success(42);
Console.WriteLine(successResult.Value); // 42

// Failure case
var error = new Error("USER_001", "User not found", ErrorType.NotFound);
var failureResult = Result<User>.Failure(error);

if (failureResult.IsFailure)
{
    Console.WriteLine(failureResult.FirstError.Message);
}

Implicit Conversions

// Implicitly convert from value to Result
Result<string> result = "Hello, World!";

// Implicitly convert from error to Result
Result<int> errorResult = new Error("ERR001", "Something went wrong");

๐Ÿ›ค๏ธ Railway-Oriented Programming

Chain operations elegantly with automatic short-circuiting on failures:

using eQuantic.Core.Outcomes.Extensions;

var result = GetUser(userId)
    .Map(user => user.Email)
    .Ensure(email => email.Contains("@"),
            Error.Validation("VAL001", "Invalid email format"))
    .Bind(email => SendWelcomeEmail(email))
    .Tap(email => _logger.LogInformation($"Email sent to {email}"));

return result.ToActionResult();

Map (Functor)

Transform the value inside a successful result:

Result<int> numberResult = Result<int>.Success(5);

Result<string> stringResult = numberResult.Map(n => n.ToString());
// Result: Success("5")

Bind (Monad / FlatMap)

Chain operations that return Results:

Result<User> GetUser(int id) { /* ... */ }
Result<Profile> GetProfile(User user) { /* ... */ }

var profileResult = GetUser(userId)
    .Bind(user => GetProfile(user));

Match (Pattern Matching)

Handle both success and failure cases:

var message = result.Match(
    onSuccess: user => $"Welcome, {user.Name}!",
    onFailure: errors => $"Error: {errors.First().Message}"
);

Ensure (Inline Validation)

Add validation inline in your chain:

var result = Result<int>.Success(5)
    .Ensure(n => n > 0, Error.Validation("VAL001", "Must be positive"))
    .Ensure(n => n < 100, Error.Validation("VAL002", "Must be less than 100"));

Tap (Side Effects)

Execute side effects without breaking the chain:

var result = GetUser(userId)
    .Tap(user => _logger.LogInformation($"User {user.Id} retrieved"))
    .TapError(errors => _logger.LogError($"Failed: {errors.First().Message}"))
    .Map(user => new UserDto(user));

โšก Async Support

All operations have async variants:

using eQuantic.Core.Outcomes.Extensions;

var result = await GetUserAsync(userId)
    .MapAsync(user => TransformUserAsync(user))
    .BindAsync(user => ValidateUserAsync(user))
    .EnsureAsync(user => CheckPermissionsAsync(user),
                 Error.Forbidden("AUTH001", "Access denied"))
    .TapAsync(user => LogActivityAsync(user));

ToResultAsync

Wrap async operations with automatic exception handling:

var result = await _httpClient
    .GetStringAsync("https://api.example.com/data")
    .ToResultAsync();

if (result.IsSuccess)
{
    var data = result.Value;
}

๐ŸŽจ Error Types

Create strongly-typed errors with semantic meaning:

// Validation errors
var error = Error.Validation("VAL001", "Email is required", "Email");

// Not found errors
var error = Error.NotFound("USER_001", "User not found", userId);

// Business rule errors
var error = Error.BusinessRule("BIZ001", "Cannot delete active subscription");

// Conflict errors
var error = Error.Conflict("CONF001", "Email already exists");

// Authorization errors
var error = Error.Unauthorized("AUTH001", "Invalid credentials");
var error = Error.Forbidden("AUTH002", "Insufficient permissions");

// Technical errors
var error = Error.Technical("TECH001", "Database connection failed", exception);

// External service errors
var error = Error.External("EXT001", "Payment gateway timeout", exception);

// From exceptions
var error = Error.FromException(exception, "ERR001", ErrorType.Technical);

Validation Errors

var validationError = new ValidationError(
    code: "VAL001",
    message: "Email format is invalid",
    propertyName: "Email",
    attemptedValue: "invalid-email"
);

๐Ÿ”— FluentValidation Integration

Optional Package: eQuantic.Core.Outcomes.FluentValidation

Seamlessly integrate FluentValidation with the Result Pattern:

Installation

dotnet add package eQuantic.Core.Outcomes.FluentValidation

Usage

using eQuantic.Core.Outcomes.FluentValidation;
using FluentValidation;

// Define your validator
public class CreateUserRequestValidator : AbstractValidator<CreateUserRequest>
{
    public CreateUserRequestValidator()
    {
        RuleFor(x => x.Email)
            .NotEmpty()
            .EmailAddress();

        RuleFor(x => x.Age)
            .GreaterThanOrEqualTo(18);
    }
}

// Use directly with validator
var validator = new CreateUserRequestValidator();
var result = validator.Validate(request);

if (result.IsSuccess)
{
    // Process valid request
}

// Or chain with Railway-Oriented Programming
var createResult = Result<CreateUserRequest>.Success(request)
    .Validate(validator)
    .Bind(req => CreateUserAsync(req))
    .Map(user => new UserDto(user));

Convert ValidationResult to Result

var validationResult = validator.Validate(request);

// Convert to Result<T>
var result = validationResult.ToResult(request);

// Or non-generic Result
var result = validationResult.ToResult();

Async Validation

// Direct async validation
var result = await validator.ValidateAsync(request);

// Chain async validation
var finalResult = await Result<CreateUserRequest>.Success(request)
    .ValidateAsync(validator)
    .BindAsync(req => CreateUserAsync(req));

// Validate Task<Result<T>>
var result = await GetUserAsync(id)
    .ValidateAsync(validator);

Integration with Existing Results

var result = GetUser(userId)
    .Validate(userValidator)  // Validate if successful
    .Bind(user => GetProfile(user.Id))
    .Validate(profileValidator)  // Chain validations
    .Map(profile => new ProfileDto(profile));

Error Mapping

FluentValidation errors are automatically converted to ValidationError:

var result = validator.Validate(invalidRequest);

result.IsFailure.Should().BeTrue();
result.Errors.Should().AllBeOfType<ValidationError>();

var emailError = result.Errors
    .OfType<ValidationError>()
    .FirstOrDefault(e => e.PropertyName == "Email");

// ValidationError properties:
// - Code (from ErrorCode)
// - Message (from ErrorMessage)
// - PropertyName
// - AttemptedValue

๐ŸŒ ASP.NET Core Integration

Automatic conversion to HTTP responses with Problem Details (RFC 7807):

using eQuantic.Core.Outcomes.AspNetCore;

[HttpGet("{id}")]
public IActionResult GetUser(int id)
{
    var result = _userService.GetById(id);
    return result.ToActionResult();
}

// Automatic mapping:
// Success โ†’ 200 OK
// NotFound โ†’ 404 Not Found with ProblemDetails
// Validation โ†’ 400 Bad Request with ValidationProblemDetails
// Unauthorized โ†’ 401 Unauthorized
// Forbidden โ†’ 403 Forbidden
// Conflict โ†’ 409 Conflict
// BusinessRule โ†’ 422 Unprocessable Entity
// Others โ†’ 500 Internal Server Error

Custom Status Codes

// Custom success status code
return result.ToActionResult(201); // 201 Created

// Created at action
return result.ToCreatedAtActionResult("GetUser", new { id = user.Id });

// No content
return result.ToNoContentResult(); // 204 No Content

๐Ÿ”— Result Combinators

Combine and aggregate multiple Results efficiently:

Combine - All or Nothing

Combine multiple results into one. If all succeed, get all values. If any fails, get all errors:

using eQuantic.Core.Outcomes.Extensions;

// Validate multiple fields in parallel
var emailResult = ValidateEmail(request.Email);
var passwordResult = ValidatePassword(request.Password);
var ageResult = ValidateAge(request.Age);

var validationResult = ResultCombinators.Combine(
    emailResult,
    passwordResult,
    ageResult
);

if (validationResult.IsSuccess)
{
    var (email, password, age) = validationResult.Value;
    // All validations passed
}
else
{
    // Contains all validation errors
    return BadRequest(validationResult.Errors);
}

Zip - Combine Different Types

Combine results of different types into tuples:

var userResult = GetUser(userId);
var profileResult = GetProfile(userId);

// Combine into tuple
var combined = userResult.Zip(profileResult);

if (combined.IsSuccess)
{
    var (user, profile) = combined.Value;
    return new UserWithProfile(user, profile);
}

// Supports up to 4 results
var result = result1.Zip(result2, result3, result4);

FirstSuccess - Fallback Pattern

Try multiple sources, return first success:

// Try cache first, then database, then API
var result = ResultCombinators.FirstSuccess(
    GetFromCache(key),
    GetFromDatabase(key),
    GetFromApi(key)
);

// Returns first successful result
// If all fail, returns failure with all errors

SuccessfulValues - Partial Success

Get only successful values, ignore failures:

var processResults = items.Select(item => ProcessItem(item));

var successful = ResultCombinators.SuccessfulValues(processResults);

// Returns Result<IEnumerable<T>> with only successful values
// Useful when some failures are acceptable

Partition - Separate Successes and Failures

Split results into successful values and errors:

var results = items.Select(item => ValidateItem(item));

var (successes, errors) = results.Partition();

Console.WriteLine($"Processed: {successes.Count()}, Failed: {errors.Count()}");

MergeErrors - Collect All Errors

Useful for validation scenarios:

var validationResults = new[]
{
    ValidateField1(),
    ValidateField2(),
    ValidateField3()
};

var merged = ResultCombinators.MergeErrors(validationResults);
// Contains all validation errors from all failed results

๐Ÿ” Observability & Tracing

Track and monitor your operations with built-in observability support:

Adding Correlation and Trace IDs

var result = await GetUserAsync(userId)
    .WithCorrelationId(httpContext.TraceIdentifier)
    .WithTraceId(Activity.Current?.Id)
    .WithMetadata("userId", userId)
    .WithMetadata("source", "api");

// Access observability data
Console.WriteLine($"CorrelationId: {result.CorrelationId}");
Console.WriteLine($"TraceId: {result.TraceId}");
Console.WriteLine($"Metadata: {result.Metadata["source"]}");

Measuring Execution Time

Automatically measure operation execution time:

// Synchronous
var result = ObservabilityExtensions.Timed(() =>
{
    return PerformExpensiveOperation();
});

Console.WriteLine($"Operation took: {result.ExecutionTime}");

// Asynchronous
var result = await ObservabilityExtensions.TimedAsync(async () =>
{
    return await PerformAsyncOperation();
});

Distributed Tracing Integration

Perfect for microservices and distributed systems:

public async Task<Result<Order>> ProcessOrderAsync(OrderRequest request, string correlationId)
{
    return await ObservabilityExtensions.TimedAsync(async () =>
    {
        var result = await _orderService.CreateOrderAsync(request);
        return result
            .WithCorrelationId(correlationId)
            .WithTraceId(Activity.Current?.Id)
            .WithMetadata("orderId", result.Value?.Id)
            .WithMetadata("service", "order-processing")
            .WithMetadata("environment", _env.EnvironmentName);
    });
}

Custom Metadata

Add any additional context to your results:

var result = Result<User>.Success(user)
    .WithMetadata("ipAddress", "192.168.1.1")
    .WithMetadata("userAgent", "Mozilla/5.0...")
    .WithMetadata("apiVersion", "2.0");

// Add multiple metadata entries at once
var metadata = new Dictionary<string, object>
{
    ["requestId"] = Guid.NewGuid(),
    ["timestamp"] = DateTimeOffset.UtcNow,
    ["region"] = "us-east-1"
};

result = result.WithMetadata(metadata);

๐Ÿ“Š Legacy Support (v1.x API)

The v1.x Builder pattern is still available for backward compatibility:

var result = Outcome.FromItemResult<User>()
    .WithSuccess()
    .WithItem(user)
    .Result();

However, we recommend migrating to the new API for better type safety and functional programming support.

๐Ÿ”„ Migration Guide (v1.x โ†’ v2.0)

Before (v1.x)

var resultBuilder = Outcome.FromItemResult<User>();
try
{
    var user = await _repository.GetByIdAsync(id);
    if (user == null)
    {
        resultBuilder = resultBuilder
            .WithError()
            .WithStatus(ResultStatus.NotFound)
            .WithMessage("User not found");
        return NotFound(resultBuilder.Result());
    }

    resultBuilder = resultBuilder
        .WithSuccess()
        .WithItem(user);
    return Ok(resultBuilder.Result());
}
catch (Exception ex)
{
    resultBuilder = resultBuilder.WithException(ex);
    return StatusCode(500, resultBuilder.Result());
}

After (v2.0)

var result = await _repository
    .GetByIdAsync(id)
    .ToResultAsync()
    .Ensure(user => user != null,
            Error.NotFound("USER_001", "User not found", id.ToString()));

return result.ToActionResult();

๐Ÿ“š Advanced Examples

Combining Multiple Results

var userResult = GetUser(userId);
var profileResult = GetProfile(userId);

var combinedResult = userResult.Bind(user =>
    profileResult.Map(profile => new UserWithProfile(user, profile))
);

Conditional Chains

var result = GetUser(userId)
    .Ensure(user => user.IsActive,
            Error.BusinessRule("BIZ001", "User is not active"))
    .Ensure(user => user.EmailVerified,
            Error.BusinessRule("BIZ002", "Email not verified"))
    .Bind(user => ProcessUser(user));

Multiple Error Accumulation

var errors = new List<IError>();

if (string.IsNullOrEmpty(request.Email))
    errors.Add(Error.Validation("VAL001", "Email is required", "Email"));

if (request.Age < 18)
    errors.Add(Error.Validation("VAL002", "Must be 18 or older", "Age"));

if (errors.Any())
    return Result<User>.Failure(errors);

return Result<User>.Success(new User(request));

๐Ÿงช Testing

The library includes comprehensive test coverage. Example:

[Fact]
public void Map_OnSuccessResult_ShouldMapValue()
{
    // Arrange
    var result = Result<int>.Success(5);

    // Act
    var mappedResult = result.Map(x => x * 2);

    // Assert
    mappedResult.IsSuccess.Should().BeTrue();
    mappedResult.Value.Should().Be(10);
}

๐Ÿ—๏ธ Architecture

  • Immutable by design - Results use C# records for immutability
  • Type-safe - Compile-time guarantees with generic types
  • Functional - Railway-Oriented Programming patterns
  • Async-first - Full Task<Result<T>> support
  • Zero allocations - Optimized for performance
  • Nullable reference types - Full C# 8+ support

๐Ÿ“– Documentation

For more examples and detailed documentation, see:

๐Ÿ”ง Development

Building & Testing

# Restore dependencies
dotnet restore

# Build
dotnet build

# Run tests
dotnet test

Versioning

This project uses GitVersion for automatic semantic versioning:

  • master: Stable releases (e.g., 2.0.0)
  • develop: Preview releases (e.g., 2.1.0-alpha.5)
  • feature/*: Feature branches (not published)
  • release/*: Beta releases (e.g., 2.0.0-beta.1)

Use Conventional Commits to control version increments:

# Patch: 2.0.0 โ†’ 2.0.1
git commit -m "fix: resolve memory leak"

# Minor: 2.0.0 โ†’ 2.1.0
git commit -m "feat: add new combinator"

# Major: 2.0.0 โ†’ 3.0.0
git commit -m "feat!: breaking API change"

Publishing

Packages are automatically published to NuGet.org via GitHub Actions:

  • Push to master: Publishes stable version
  • Push to develop: Publishes preview version
  • Create tag (e.g., v2.0.1): Publishes specific version

๐Ÿค Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

๐Ÿ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

๐Ÿ™ Acknowledgments

Inspired by functional programming patterns from:

  • Railway-Oriented Programming (Scott Wlaschin)
  • Result types in Rust, F#, and Haskell
  • FluentResults, ErrorOr, and other .NET libraries

eQuantic Systems ยฉ 2019-2025

Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  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 was computed.  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 was computed.  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 (1)

Showing the top 1 NuGet packages that depend on eQuantic.Core.Outcomes:

Package Downloads
eQuantic.Core.Outcomes.FluentValidation

FluentValidation integration for eQuantic Core Outcomes Library

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.0 200 10/9/2025
1.0.1.2 1,099 2/26/2019
1.0.1.1 741 2/24/2019
1.0.0.3 752 2/23/2019
1.0.0.1 880 2/18/2019

v2.0.0 - Major update: .NET 6/8 support, immutable records, Railway-Oriented Programming, typed errors, async support