SGuard 0.1.2
dotnet add package SGuard --version 0.1.2
NuGet\Install-Package SGuard -Version 0.1.2
<PackageReference Include="SGuard" Version="0.1.2" />
<PackageVersion Include="SGuard" Version="0.1.2" />
<PackageReference Include="SGuard" />
paket add SGuard --version 0.1.2
#r "nuget: SGuard, 0.1.2"
#:package SGuard@0.1.2
#addin nuget:?package=SGuard&version=0.1.2
#tool nuget:?package=SGuard&version=0.1.2
SGuard
π¬ Join the Community Chat
Join our community chat to ask questions, share feedback, or get involved: #sguard:gitter.im
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 without throwing exceptions.
- Throwing Guards (ThrowIf.*): Throw exceptions when conditions are met, withCallerArgumentExpression-powered messages.
- Any & All Guards: Predicate-based validation for collections.
- Comprehensive Comparison Guards: Between,LessThan,LessThanOrEqual,GreaterThan,GreaterThanOrEqualfor generics and strings (withStringComparison).
- Null/Empty Checks: Deep and type-safe null/empty validation for primitives, collections, and complex types.
- Custom Exception Support: Overloads for custom exception types, with constructor argument support.
- Callback Model: Unified SGuardCallbackandGuardOutcomefor success/failure handling.
- Expression Caching: Efficient, thread-safe caching for compiled expressions.
- Rich Exception Messages: Informative diagnostics using CallerArgumentExpression.
- Multi-targeting: Supports .NET 6, 7, 8, and 9.
π 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.
 
- Callback exceptions are safely swallowed, so your validation flow isnβt disrupted.
 
- A single SGuardCallback(outcome) works across both APIs:
- 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.
 
 
- Throw built-in exceptions for common guards or supply your own:
- Expressive, dual API - Choose the style that fits your code:
- Is.* returns booleans for control-flow friendly checks.
- ThrowIf.* it fails fast with informative exceptions when rules are violated.
 
 
- Choose the style that fits your code:
- Culture-aware comparisons and inclusive ranges - String overloads accept StringComparison for correct cultural/ordinal semantics.
- Between checks are inclusive by design for predictable validation.
 
- Performance and ergonomics - Expression caching reduces overhead for repeated checks.
- Minimal allocations and thread-safe evaluation where applicable.
 
- Modern .NET support - Targets .NET 6, 7, 8, and 9 with multi-targeting, ensuring broad compatibility.
 
β‘ Quick Start
SGuard helps you validate inputs and state with two complementary APIs:
- ThrowIf.*: fail fast by throwing informative exceptions.
- 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.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;
    }
}
2) Check conditions (boolean style)
if (Is.Between(value, min, max)) { /* ... */ }
if (Is.LessThan(a, b)) { /* ... */ }
if (Is.Any(list, x => x > 0)) { /* ... */ }
if (!Is.Between(req.Age, 13, 130))
{
    throw new ArgumentOutOfRangeException(nameof(req.Age), "Age seems invalid.");
}
// Numeric comparisons 
bool inRange = Is.Between(value, min, max); 
bool isLess = Is.LessThan(a, b); 
bool isGreaterOrEqual = Is.GreaterThanOrEqual(a, b);
bool before = Is.LessThan("straΓe", "strasse", StringComparison.InvariantCulture); // culture-aware
// Collections 
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);
bool lessIgnoreCase = Is.LessThan("Apple", "banana", StringComparison.OrdinalIgnoreCase)
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")));
ThrowIf.LessThan(5, 2, SGuardCallbacks.OnSuccess(() => logger.LogInformation("a >= b OK")));
// 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<string, string, string, MyCustomException>(value, min, max, new MyCustomException("Out of range!"));
// 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);
// Throw if the ordering violates your rule
ThrowIf.GreaterThan("zebra", "apple", StringComparison.Ordinal); // throws (zebra > apple)
6) Notes
- Between is inclusive (min and max are allowed).
- 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).
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 and never throw for the check itself.
- Outcome = Success when the result is true, Outcome = Failure when the result is false.
 
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 is invoked regardless of the outcome of the guard.
// 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;
ThrowIf.LessThan(1, 2, outcome => observed = outcome); // throws -> observed remains null (callback still runs with Failure before exception propagation)
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
ThrowIf.LessThan(a, b, () => Console.WriteLine("Failed!"));
ThrowIf.Any(list, x => x == null);
// Optionally run a callback on failure (e.g., logging/metrics/cleanup)
ThrowIf.GreaterThan(total, limit, () => 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
        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, throw an exception with a custom message and invoke the callback.
    ThrowIf.NullOrEmpty(username, callback);
    
    // Proceed with saving the user...
}
public void UpdateEmail(string email)
{
    var onSuccess = SGuardCallbacks.OnSuccess(() =>
        audit.Record("Email validation succeeded"));
    // If valid, onSuccess is called; if not, an exception is thrown
    ThrowIf.NullOrEmpty(email, onSuccess);
    // Proceed with updating the email...
}
β Test and Coverage Status
Test and Coverage Status
Test Results
| Total | Passed | Failed | Skipped | 
|---|---|---|---|
| 367 | 367 | 0 | 0 | 
Code Coverage
Summary
| Generated on: | 09/05/2025 - 10:39:42 | 
| Coverage date: | 09/03/2025 - 19:56:53 - 09/05/2025 - 10:39:39 | 
| Parser: | MultiReport (12x Cobertura) | 
| Assemblies: | 1 | 
| Classes: | 15 | 
| Files: | 50 | 
| Line coverage: | 86.9% (815 of 937) | 
| Covered lines: | 815 | 
| Uncovered lines: | 122 | 
| Coverable lines: | 937 | 
| Total lines: | 4360 | 
| Branch coverage: | 83% (186 of 224) | 
| Covered branches: | 186 | 
| Total branches: | 224 | 
| Method coverage: | Feature is only available for sponsors | 
| Name | Covered | Uncovered | Coverable | Total | Line coverage | Covered | Total | Branch coverage | 
|---|---|---|---|---|---|---|---|---|
| SGuard | 815 | 122 | 937 | 4360 | 86.9% | 186 | 224 | 83% | 
| SGuard.ExceptionActivator | 15 | 0 | 15 | 56 | 100% | 6 | 8 | 75% | 
| SGuard.Exceptions.AllException | 0 | 4 | 4 | 44 | 0% | 0 | 0 | |
| SGuard.Exceptions.AnyException | 2 | 2 | 4 | 36 | 50% | 0 | 0 | |
| SGuard.Exceptions.BetweenException | 23 | 6 | 29 | 135 | 79.3% | 0 | 0 | |
| SGuard.Exceptions.GreaterThanException | 17 | 6 | 23 | 108 | 73.9% | 0 | 0 | |
| SGuard.Exceptions.GreaterThanOrEqualException | 17 | 6 | 23 | 110 | 73.9% | 0 | 0 | |
| SGuard.Exceptions.LessThanException | 15 | 6 | 21 | 111 | 71.4% | 0 | 0 | |
| SGuard.Exceptions.LessThanOrEqualException | 15 | 6 | 21 | 111 | 71.4% | 0 | 0 | |
| SGuard.Exceptions.NullOrEmptyException | 15 | 4 | 19 | 98 | 78.9% | 0 | 0 | |
| SGuard.Is | 181 | 0 | 181 | 1100 | 100% | 22 | 24 | 91.6% | 
| SGuard.SGuard | 31 | 1 | 32 | 124 | 96.8% | 12 | 12 | 100% | 
| SGuard.SGuardCallbacks | 2 | 2 | 4 | 68 | 50% | 0 | 0 | |
| SGuard.Throw | 21 | 0 | 21 | 243 | 100% | 0 | 0 | |
| SGuard.ThrowIf | 311 | 19 | 330 | 1558 | 94.2% | 52 | 56 | 92.8% | 
| SGuard.Visitor.NullOrEmptyVisitor | 150 | 60 | 210 | 458 | 71.4% | 94 | 124 | 75.8% | 
π’ Versioning
This project follows Semantic Versioning. As of this release, versioning restarts at 0.1.0. If you previously consumed older versions, please upgrade to the latest package.
π€ 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.
π Links
| Product | Versions 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 is compatible. 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 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 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. | 
- 
                                                    net6.0- No dependencies.
 
- 
                                                    net7.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.
# Release Notes - Version 0.1.2
            * Added a "📊 Benchmarks" section to the main README.md, providing a direct link to the SGuard.Benchmark/benchmarks/ folder for easy developer access to performance results. (#28)
            * Ensured all benchmark results are discoverable and documented for each guard method (Is.* and ThrowIf.*), including All, Any, Between, GreaterThan, LessThan, and NullOrEmpty.
            * No breaking changes to the core library or APIs.
            * Improved developer experience and documentation clarity.
            This update makes it much easier for contributors and users to find and review performance benchmarks for all guard methods.