eQuantic.Core.Outcomes.FluentValidation 2.0.0

dotnet add package eQuantic.Core.Outcomes.FluentValidation --version 2.0.0
                    
NuGet\Install-Package eQuantic.Core.Outcomes.FluentValidation -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.FluentValidation" 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.FluentValidation" Version="2.0.0" />
                    
Directory.Packages.props
<PackageReference Include="eQuantic.Core.Outcomes.FluentValidation" />
                    
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.FluentValidation --version 2.0.0
                    
#r "nuget: eQuantic.Core.Outcomes.FluentValidation, 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.FluentValidation@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.FluentValidation&version=2.0.0
                    
Install as a Cake Addin
#tool nuget:?package=eQuantic.Core.Outcomes.FluentValidation&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

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.0 161 10/9/2025

v2.0.0 - FluentValidation integration for Result Pattern