Skip to content

[FEATURE] Add Guards.TryValidateNonNegative<T> ROP guard clause #653

@dlrivada

Description

@dlrivada

Summary

Add Guards.TryValidateNonNegative<T> to Encina.GuardClauses to provide an ROP-compatible equivalent of .NET 8's ArgumentOutOfRangeException.ThrowIfNegative<T>.

Motivation

.NET 8 introduced ArgumentOutOfRangeException.ThrowIfNegative<T>() which validates that a value is >= 0. However, this is exception-based and incompatible with Encina's Railway Oriented Programming (ROP) pattern using Either<EncinaError, T>.

Currently, Encina has TryValidatePositive (> 0) but lacks TryValidateNonNegative (>= 0). This is a common need in domain modeling where zero is a valid value (e.g., account balance, discount percentage, stock quantity).

Priority: High

Use Cases

  • Validating account balances (zero balance is valid, negative is not)
  • Validating discount percentages (0% is valid, -5% is not)
  • Validating stock quantities after decrement operations

Proposed Solution

/// <summary>
/// Validates that a value is not negative (greater than or equal to zero).
/// </summary>
public static bool TryValidateNonNegative<T>(
    T value,
    string paramName,
    out EncinaError error,
    string? message = null)
    where T : IComparable<T>

Usage Example

// Try-pattern (imperative)
if (!Guards.TryValidateNonNegative(balance, nameof(balance), out var error))
    return Left<EncinaError, AccountId>(error);

// Or with Ensure (functional)
return Right<EncinaError, decimal>(balance)
    .Ensure(b => b >= 0m, b => EncinaErrors.Create("guard.validation_failed",
        $"balance must be non-negative, was {b}"));

Alternatives Considered

  1. Use TryValidate(value >= 0, ...) with manual condition: Works but loses semantic metadata (guard type, actual value) in the error details.
  2. Use TryValidateInRange(value, 0, max, ...): Requires knowing the max value, which is often not available.

Affected Packages

  • Encina.GuardClauses

Test Matrix

Test Type Required? Scope Notes
UnitTests Required Success/failure paths, edge cases (zero, negative, positive), custom messages
GuardTests Required Null parameter checks
PropertyTests Recommended Invariant: returns true iff value >= 0

Implementation Tasks

  • Add Guards.TryValidateNonNegative<T> method to Guards.cs
  • Add comprehensive XML documentation with examples
  • Add unit tests (positive, zero, negative values, custom messages)
  • Add guard clause tests
  • Add property-based tests
  • Update README.md with new guard documentation
  • Update PublicAPI.Unshipped.txt

Acceptance Criteria

  • Method implemented: returns true for values >= 0, false for negative values
  • Metadata includes guard type (NonNegative), parameter name, actual value, type
  • Custom message support via optional parameter
  • Comprehensive unit tests
  • XML doc comments with examples
  • Zero build warnings
  • README updated

.NET Equivalent

ArgumentOutOfRangeException.ThrowIfNegative<T>(T value, string? paramName) (since .NET 8)

Related Issues

Part of ROP guard clause parity with .NET 8-10 built-in throw helpers.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-domain-modelingDomain modeling building blocks (Entities, Value Objects, Aggregates)area-validationValidation patterns and providersdotnet-10Leverages .NET 10 new featuresenhancementNew feature or requestpriority-highPriority: High (⭐⭐⭐⭐)

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions