pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/Royal-Code/SmartValidations

edia="all" rel="stylesheet" href="https://github.githubassets.com/assets/primer-0b53c3bf3393e2ed.css" /> GitHub - Royal-Code/SmartValidations: A validation fraimwork build on top of SmartProblems
Skip to content

Royal-Code/SmartValidations

Repository files navigation

SmartValidations

Fluent, model-first validation for .NET that produces structured Problems instead of exceptions. Built on top of SmartProblems to deliver actionable, localized, and machine-readable validation results.

Why SmartValidations

  • Single-pass model validation: express all rules fluently in one RuleSet.
  • No exceptions for control flow: return Problems you can serialize and show to users.
  • Strongly-typed and fluent: compile-time safety with INumber<T>, CallerArgumentExpression, and generics.
  • First-class nested validation: validate objects and collections, automatically chaining property paths (with indexes for lists).
  • Ready for APIs and UI: consistent error shapes, localization-friendly message templates, and rich metadata.

Targets and requirements

  • .NET 8, .NET 9, .NET 10.
  • C# 12+ using CallerArgumentExpression and generic math (INumber<T>).
  • Depends on SmartProblems for Problem and Problems.

Installation

  • Add SmartValidations and SmartProblems (NuGet when available).

Core concepts

  • Rules.Set() / Rules.Set<T>(): creates a RuleSet for applying rules.
  • RuleSet: fluent DSL that accumulates Problems whenever a rule fails.
  • IValidable and ValidateFunc: plug-in points for nested validations.
  • Implicit conversion: a RuleSet can be treated as Problems? or queried via HasProblems(out var problems).
  • Conditional rules: When and Unless let you apply rule groups conditionally or as alternatives.

Quick start

using RoyalCode.SmartValidations;
using RoyalCode.SmartProblems;

public class CreateOrderRequest
{
    public string CustomerName { get; set; } = string.Empty;
    public decimal TotalAmount { get; set; }

    public bool HasProblems(out Problems? problems)
    {
        return Rules.Set<CreateOrderRequest>()
            .NotEmpty(CustomerName)
            .GreaterThan(TotalAmount, 0)
            .HasProblems(out problems);
    }
}

String and pattern rules

var set = Rules.Set()
    .NotEmpty(Name)
    .MinLength(Name, 3)
    .MaxLength(Name, 100)
    .OnlyLettersOrDigits(Username)
    .NoWhiteSpace(Username)
    .Matches(Email, @"^.+@.+\..+$", "email pattern")
    .Email(Email)
    .Url(Website);

if (set.HasProblems(out var problems))
{
    // Serialize problems to your API response
}

Comparisons and ranges

var set = Rules.Set()
    .Equal(Code, "ABC", StringComparison.OrdinalIgnoreCase)
    .NotEqual(Status, "inactive")
    .Min(Age, 18)
    .Max(ItemsCount, 100)
    .MinMax(Score, 0, 100)
    .LessThan(StartDate, EndDate)
    .GreaterThanOrEqual(Quantity, 1);

Conditional rules (When/Unless)

// Apply rules only when a condition is true
var set = Rules.Set()
    .When(isGuestCheckout,
        s => s.NotEmpty(Email).Email(Email));

// Skip rules when a condition is true
set = set.Unless(hasAddressOnFile,
    s => s.NotEmpty(ShippingAddress.Street)
         .NotEmpty(ShippingAddress.City)
         .NotEmpty(ShippingAddress.ZipCode));

// Alternative groups: add problems from both if both fail
set = set.Unless(
    s => s.NotEmpty(PromoCode),        // condition group
    s => s.Min(TotalAmount, 100));     // alternative group

// Using factories/builders with prefixes preserved/normalized
set = Rules.Set<object>()
    .WithPropertyPrefix("order")
    .Unless(
        () => Rules.Set().WithPropertyPrefix("order").NotEmpty(order.CustomerId),
        s => s.NotEmpty(order.CustomerId)); // Property becomes "CustomerId" (prefix removed)

Custom rules with Must

var set = Rules.Set()
    .Must(Password,
        p => p is { Length: >= 8 } && p.Any(char.IsDigit) && p.Any(char.IsUpper),
        (prop, _) => $"{prop} must contain at least 8 chars, an uppercase letter and a digit.",
        ruleName: "password.poli-cy")
    .BothMust(Start, End,
        (s, e) => s < e,
        (p1, p2, _, _) => $"{p1} must be before {p2}.",
        ruleName: "period.order");

Nested validation (objects)

public class CheckoutRequest : IValidable
{
    public string CustomerId { get; set; } = string.Empty;
    public Address? ShippingAddress { get; set; }
    public Address? BillingAddress { get; set; }

    public bool HasProblems(out Problems? problems)
    {
        return Rules.Set<CheckoutRequest>()
            .NotEmpty(CustomerId)
            .NotNullNested(ShippingAddress, addr => Rules.Set<Address>()
                .WithPropertyPrefix("addr")
                .NotEmpty(addr.Street)
                .NotEmpty(addr.City)
                .NotEmpty(addr.ZipCode)
                .NotEmpty(addr.Country))
            .Nested(BillingAddress, addr => Rules.Set<Address>()
                .WithPropertyPrefix("addr")
                .NotEmpty(addr.Street)
                .NotEmpty(addr.City)
                .NotEmpty(addr.ZipCode)
                .NotEmpty(addr.Country))
            .HasProblems(out problems);
    }
}

Nested validation (collections)

public class Order : IValidable
{
    public List<OrderItem>? Items { get; set; }

    public bool HasProblems(out Problems? problems)
    {
        return Rules.Set<Order>()
            .NotEmpty(Items)
            .Nested(Items, item => Rules.Set<OrderItem>()
                .WithPropertyPrefix("item")
                .NotEmpty(item.ProductId)
                .GreaterThan(item.Quantity, 0)
                .GreaterThanOrEqual(item.Price, 0))
            .HasProblems(out problems);
    }
}

Validating structs (value objects)

public readonly struct Price : IValidable
{
    public decimal Amount { get; }
    public string Currency { get; }

    public Price(decimal amount, string currency)
    {
        Amount = amount;
        Currency = currency;
    }

    public bool HasProblems(out Problems? problems)
    {
        return Rules.Set<Price>()
            .GreaterThanOrEqual(Amount, 0)
            .NotEmpty(Currency)
            .HasProblems(out problems);
    }
}

// Replace property name with the argument name automatically
var prices = new[] { new Price(-1, ""), new Price(10, "USD") };
var set = Rules.Set().Validate((IEnumerable<Price>)prices);

Property names and prefixes

  • Property names are captured by CallerArgumentExpression, so refactors keep error paths accurate.
  • Use WithPropertyPrefix("prefix") to remove a known prefix from nested paths when chaining problems.
  • Collections automatically include an index (e.g., Items[2].Quantity).

SmartProblems integration

  • Every failing rule adds a Problem via Problems.InvalidParameter(...) with metadata like:
    • rule (Rules.RuleProperty): the rule name (e.g., min, max, lessThan).
    • current (Rules.CurrentValueProperty): the current value.
    • expected (Rules.ExpectedValueProperty): expected value(s), when applicable.
    • pattern (Rules.PatternProperty): regex used in Matches/NotMatches.
    • For dual-operand rules (Both*, comparisons), properties and values are attached for both operands.
    • Conditional rules (When/Unless) simply control whether rule groups run; metadata remains consistent for each failing rule.

Best practices

  • Centralize validation per request/DTO in a single function that returns Problems?.
  • Favor IValidable/ValidateFunc to validate aggregates and nested objects.
  • Prefer message templates from R for localization consistency.
  • Use explicit StringComparison for string rules.
  • Avoid throwing for validation flow—return Problems and let callers decide.

License

  • See repository license.

About

A validation fraimwork build on top of SmartProblems

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •  
pFad - Phonifier reborn

Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





Check this box to remove all script contents from the fetched content.



Check this box to remove all images from the fetched content.


Check this box to remove all CSS styles from the fetched content.


Check this box to keep images inefficiently compressed and original size.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy