SGuard 0.2.0

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

SGuard

CI NuGet NuGet Downloads License: MIT Matrix Chat

SGuard is a lightweight, extensible guard clause library for .NET, providing expressive and robust validation for method arguments, object state, and business rules. It offers both boolean checks (Is.*) and exception-throwing guards (ThrowIf.*), with a unified callback model and rich exception diagnostics.

πŸš€ Features

  • Boolean Guards (Is.*): Check conditions and get a bool back instead of an exception.
  • Throwing Guards (ThrowIf.*): Throw when a condition is true, with CallerArgumentExpression-powered messages.
  • Any & All Guards: Predicate-based validation for collections (IEnumerable<T> and ReadOnlySpan<T>).
  • Comparison Guards: Between (inclusive), LessThan, LessThanOrEqual, GreaterThan, GreaterThanOrEqual for any IComparable<T> type. The Is.* comparisons and ThrowIf.Between also have string overloads that take a StringComparison. With a floating-point NaN operand, Is.* comparisons return false and ThrowIf.* comparisons throw.
  • Null/Empty Checks: Null, default values (0, Guid.Empty, ...), empty strings (whitespace is not empty), collections and spans. With a selector (o => o.Customer.Email), SGuard follows the member path; a complex-type member counts as empty only when all of its readable properties are null or empty.
  • Email Validation: Is.Email with a built-in pattern or your own regex (with a match timeout).
  • Custom Exception Support: Overloads for custom exception instances and types, with constructor argument support.
  • Callback Model: Unified SGuardCallback and GuardOutcome for success/failure handling.
  • Expression Caching: Selectors are compiled once and cached by expression structure (thread-safe).
  • Clear Exception Messages: Built-in exceptions derive from ArgumentException and name the failing argument expression; checked values are left out of messages by default.
  • Multi-targeting: Supports .NET 8, 9, and 10.

πŸ“Š Benchmarks

Performance benchmarks for all guard methods are available in the SGuard.Benchmark/benchmarks/ folder. Explore these to see real-world performance comparisons for Is.* and ThrowIf.* methods.

πŸ“¦ Installation

dotnet add package SGuard

πŸ€” Why SGuard?

  • Clear diagnostics

    • Uses CallerArgumentExpression to produce precise, helpful error messages that point to the exact argument/expression that failed.
  • Consistent callback model

    • A single SGuardCallback(outcome) works across both APIs:
      • ThrowIf.* invokes with Failure when it’s about to throw, Success when it passes.
      • Is.* invokes with Success when the result is true, Failure when false (so Is.NullOrEmpty((string?)null) reports Success).
    • Callback exceptions are safely swallowed, so your validation flow isn’t disrupted.
  • Rich exception surface

    • Throw built-in exceptions for common guards or supply your own:
      • Pass a custom exception instance, use a generic TException, or provide constructor arguments for detailed messages.
  • Expressive, dual API

    • Choose the style that fits your code:
      • Is.* returns booleans for control-flow friendly checks.
      • ThrowIf.* fails fast with informative exceptions when rules are violated.
  • Culture-aware comparisons and inclusive ranges

    • String overloads of the Is.* comparisons and ThrowIf.Between accept StringComparison for correct cultural/ordinal semantics.
    • Between checks are inclusive by design for predictable validation.
  • Performance and ergonomics

    • Selector expressions are compiled once and cached by expression structure, so repeated checks don't pay the compilation cost again (selectors that capture local variables are still compiled on every call).
    • The selector cache is thread-safe.
  • Modern .NET support

    • Targets .NET 8, 9, and 10 with multi-targeting.

⚑ Quick Start

SGuard helps you validate inputs and state with two complementary APIs:

  • ThrowIf.*: fail fast by throwing informative exceptions when a condition is true.
  • Is.*: return booleans for control-flow-friendly checks.

1) Validate inputs (fail fast)

public record CreateUserRequest(string Username, int Age, string Email);

public User CreateUser(CreateUserRequest req)
{
    ThrowIf.NullOrEmpty(req);
    ThrowIf.NullOrEmpty(req.Email);
    ThrowIf.NullOrEmpty(req.Username);
    ThrowIf.LessThan(req.Age, 13, new ArgumentException("User must be 13+.", nameof(req.Age)));

    // Optionally check formats or ranges
    if (!Is.Email(req.Email))
        throw new ArgumentException("Email is not valid.", nameof(req.Email));

    if (!Is.Between(req.Age, 13, 130))
        throw new ArgumentOutOfRangeException(nameof(req.Age), "Age seems invalid.");

    return new User(req.Username, req.Age, req.Email);
}

public sealed class User
{
    public User(string username, int age, string email)
    {
        ThrowIf.LessThan(age, 0);
        ThrowIf.NullOrEmpty(email);
        ThrowIf.NullOrEmpty(username);

        Age = age;
        Email = email;
        Username = username;
    }

    public int Age { get; }
    public string Email { get; }
    public string Username { get; }
}

2) Check conditions (boolean style)

if (Is.Between(value, min, max)) { /* ... */ }
if (Is.LessThan(a, b)) { /* ... */ }
if (Is.Any(list, x => x > 0)) { /* ... */ }

// Numeric comparisons
bool inRange = Is.Between(value, min, max);
bool isLess = Is.LessThan(a, b);
bool isGreaterOrEqual = Is.GreaterThanOrEqual(a, b);

// Collections (LINQ semantics: Is.All(empty) is true, Is.Any(empty) is false)
bool anyPositive = Is.Any(numbers, n => n > 0);
bool allNonNull = Is.All(items, it => it is not null);

// Strings (culture/ordinal aware)
bool lessOrdinal = Is.LessThan("apple", "banana", StringComparison.Ordinal);            // true
bool lessIgnoreCase = Is.LessThan("Apple", "banana", StringComparison.OrdinalIgnoreCase); // true

// Email (built-in ASCII pattern, at most 254 characters; not a full RFC 5322 parser)
bool validEmail = Is.Email("jane.doe@example.com"); // true

Is.* methods don't throw for the check itself, but they do throw for invalid arguments: ArgumentNullException for a null operand, predicate, source or Is.Email(null); ArgumentException for Is.Email("") and for Between bounds where min > max; and RegexMatchTimeoutException when a custom Is.Email pattern times out.

3) Callbacks (side effects on success/failure)

// ThrowIf: run side effects on the outcome
ThrowIf.LessThan(1, 2, SGuardCallbacks.OnFailure(() => logger.LogWarning("a < b failed")));   // logs, then throws
ThrowIf.LessThan(5, 2, SGuardCallbacks.OnSuccess(() => logger.LogInformation("a >= b OK"))); // logs, no throw

// Is: outcome maps to the boolean result (true=Success, false=Failure)
bool ok = Is.Between(5, 1, 10, SGuardCallbacks.OnSuccess(() => metrics.Increment("is.between.true")));

4) Custom exceptions

ThrowIf.LessThanOrEqual(a, b, new MyCustomException("Invalid!"));

// ThrowIf.Between throws when the value is INSIDE the range (inclusive)
ThrowIf.Between(port, 0, 1023, new MyCustomException("Well-known ports are reserved."));

// Throw using your own exception type
ThrowIf.Any(items, i => i is null, new DomainValidationException("Collection contains null item(s)."));

// Another example with range validation
ThrowIf.LessThanOrEqual(quantity, 0, new DomainValidationException("Quantity must be greater than zero."));

5) String comparisons (culture/ordinal aware)

// Ordinal comparisons
bool before = Is.LessThan("apple", "banana", StringComparison.Ordinal); // true

// ThrowIf.Between has a StringComparison overload (throws when the value is inside the range)
ThrowIf.Between("kiwi", "a", "m", StringComparison.OrdinalIgnoreCase); // throws BetweenException

ThrowIf.LessThan/GreaterThan and their OrEqual variants have no StringComparison overload. Don't use lexicographic string comparison for access-control, path-prefix or version checks: use Path.GetFullPath with an ordinal StartsWith on a root that ends with a separator for paths, and System.Version for versions.

6) Notes

  • Between is inclusive (min and max are allowed), and throws ArgumentException when min > max.
  • ThrowIf invokes callbacks with Failure when it’s about to throw, Success when it passes.
  • Is.* invokes callbacks with Success when the result is true, Failure when false.
  • Callback exceptions are swallowed (they won’t break your validation flow).
  • A floating-point NaN operand (double, float, Half) makes Is.* comparisons return false and ThrowIf.* comparisons throw. A check such as if (Is.GreaterThan(x, max)) reject(); therefore lets NaN through; prefer ThrowIf.* or !Is.Between(...).

Exception messages and options

Built-in exceptions (NullOrEmptyException, BetweenException, GreaterThanException, LessThanException, ... in SGuard.Exceptions) derive from ArgumentException, so existing catch (ArgumentException) blocks handle them. ParamName holds the caller's argument expression, and the message names the expressions but leaves the checked values out:

ThrowIf.NullOrEmpty(request.Name);
// NullOrEmptyException: Value 'request.Name' is null or empty.

ThrowIf.GreaterThan(request.Age, limit);
// GreaterThanException: Left value is greater than right value. Actual: left=request.Age, right=limit.

To include the values (converted with ToString() and truncated to 64 characters) in Message and Exception.Data, opt in once at startup. Only do this when the checked values can't be secrets or personal data:

SGuardOptions.IncludeValuesInExceptions = true;
// GreaterThanException: '4217' is greater than '1000'. Actual: left=request.Age, right=limit.

Callbacks – When do they run?

  • ThrowIf methods:
    • Outcome = Failure β†’ the guard is about to throw (callback runs just before the exception propagates).
    • Outcome = Success β†’ the guard passes (no exception is thrown).
    • If the API fails due to invalid arguments (e.g., null selector or null exception instance), the callback is NOT invoked.
Examples:
// Failure β†’ throws β†’ OnFailure runs
ThrowIf.LessThan(1, 2, SGuardCallbacks.OnFailure(() => logger.LogWarning("a < b failed")));

// Success β†’ no throw β†’ OnSuccess runs
ThrowIf.LessThan(5, 2, SGuardCallbacks.OnSuccess(() => logger.LogInformation("a >= b OK")));
  • Is methods:
    • Return a boolean; they throw only for invalid arguments (see above), not for the check itself.
    • Outcome = Success when the result is true, Outcome = Failure when the result is false. Success means "the method returned true", not "validation passed": Is.NullOrEmpty((string?)null) reports Success.
Examples
// True β†’ OnSuccess runs
bool inRange = Is.Between(5, 1, 10, SGuardCallbacks.OnSuccess(() => metrics.Increment("is.between.true")));

// False β†’ OnFailure runs
bool isLess = Is.LessThan(5, 2, SGuardCallbacks.OnFailure(() => metrics.Increment("is.lt.false")));
Combine callbacks (Success + Failure)
var onFailure = SGuardCallbacks.OnFailure(() => notifier.Notify("Validation failed"));
var onSuccess = SGuardCallbacks.OnSuccess(() => notifier.Notify("Validation passed"));
SGuardCallback combined = onFailure + onSuccess;

// If inside range -> throws -> Failure -> only onFailure runs
// If outside range -> no throw -> Success -> only onSuccess runs
ThrowIf.Between(value, min, max, combined);

Note: The callback runs for both outcomes, but not when the call itself is invalid:

// Passing a null exception instance causes an immediate ArgumentNullException.
// The callback is NOT invoked in this case (no Success/Failure outcome is produced).
try
{
    ThrowIf.Between<int, int, int, InvalidOperationException>(
        5, 1, 10,
        (InvalidOperationException)null!, // invalid argument
        SGuardCallbacks.OnFailure(() => logger.LogError("won't run")));
}
catch (ArgumentNullException)
{
    // expected, and callback not called
}

Inline callback when you need the outcome value directly

GuardOutcome? observed = null;

try
{
    ThrowIf.LessThan(1, 2, outcome => observed = outcome); // throws LessThanException
}
catch (LessThanException)
{
    // observed == GuardOutcome.Failure: the callback ran before the exception propagated
}

More Examples

Throwing Guards
ThrowIf.NullOrEmpty(str);
ThrowIf.NullOrEmpty(obj, x => x.Property);
ThrowIf.Between(value, min, max); // Throws if value is between min and max (inclusive)
ThrowIf.LessThan(a, b, SGuardCallbacks.OnFailure(() => Console.WriteLine("Failed!")));
ThrowIf.Any(list, x => x == null);

// Optionally run a callback on failure (e.g., logging/metrics/cleanup)
ThrowIf.GreaterThan(total, limit, SGuardCallbacks.OnFailure(() => logger.LogWarning("Limit exceeded")));

// With selector for nested properties (CallerArgumentExpression helps messages)
ThrowIf.NullOrEmpty(order, o => o.Customer.Name);

πŸ“ Usage Examples (Real-life Scenarios)

public static class CheckoutService
{
    public static void ValidateCart(Cart cart, IReadOnlyDictionary<string, int> stockBySku)
    {
        ThrowIf.NullOrEmpty(cart);
        ThrowIf.NullOrEmpty(cart.Items);

        // Every item must have positive quantity
        if (!Is.All(cart.Items, i => i.Quantity > 0))
            throw new ArgumentException("All items must have a positive quantity.", nameof(cart.Items));

        // Check stock levels
        foreach (var item in cart.Items)
        {
            var stock = stockBySku.TryGetValue(item.Sku, out var s) ? s : 0;
            ThrowIf.GreaterThan(item.Quantity, stock, new InvalidOperationException($"Insufficient stock for SKU '{item.Sku}'."));
        }

        // Totals (decimal needs a decimal literal: 0m)
        ThrowIf.LessThanOrEqual(cart.TotalAmount, 0m, new ArgumentOutOfRangeException(nameof(cart.TotalAmount), "Total must be greater than zero."));
    }
}

public void SaveUser(string username)
{
    var callback = SGuardCallbacks.OnFailure(() =>
        logger.LogWarning("Validation failed: username is required"));

    // When username is null or empty, the callback runs and a NullOrEmptyException is thrown.
    ThrowIf.NullOrEmpty(username, callback);

    // Proceed with saving the user...
}

public void UpdateEmail(string email)
{
    var onSuccess = SGuardCallbacks.OnSuccess(() =>
        audit.Record("Email validation succeeded"));

    // If email is not null or empty, onSuccess is called; otherwise an exception is thrown
    ThrowIf.NullOrEmpty(email, onSuccess);

    // Proceed with updating the email...
}

πŸ’¬ Join the Community Chat

Join our community chat to ask questions, share feedback, or get involved: #sguard:gitter.im

🀝 Contributing

Contributions are welcome! Please see CONTRIBUTING.md for guidelines.

🌐 Code of Conduct

This project adheres to the .NET Foundation Code of Conduct. By participating, you are expected to uphold this code.

πŸ“œ License

This project is licensed under the MIT License, a permissive open source license. See the LICENSE file for details.

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.
  • net10.0

    • No dependencies.
  • net8.0

    • No dependencies.
  • net9.0

    • No dependencies.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on SGuard:

Package Downloads
SGuard.DataAnnotations

Advanced, extensible, and multilingual data validation and guard clause library for .NET. Includes custom validation attributes, guard helpers, and resource-based error messages for enterprise-grade applications.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.3.0 38 9/26/2026
0.2.0 48 9/26/2026
0.1.2 303 9/14/2025
0.1.1 381 9/5/2025