SGuard 0.2.0
See the version list below for details.
dotnet add package SGuard --version 0.2.0
NuGet\Install-Package SGuard -Version 0.2.0
<PackageReference Include="SGuard" Version="0.2.0" />
<PackageVersion Include="SGuard" Version="0.2.0" />
<PackageReference Include="SGuard" />
paket add SGuard --version 0.2.0
#r "nuget: SGuard, 0.2.0"
#:package SGuard@0.2.0
#addin nuget:?package=SGuard&version=0.2.0
#tool nuget:?package=SGuard&version=0.2.0
SGuard
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 aboolback instead of an exception. - Throwing Guards (
ThrowIf.*): Throw when a condition is true, withCallerArgumentExpression-powered messages. - Any & All Guards: Predicate-based validation for collections (
IEnumerable<T>andReadOnlySpan<T>). - Comparison Guards:
Between(inclusive),LessThan,LessThanOrEqual,GreaterThan,GreaterThanOrEqualfor anyIComparable<T>type. TheIs.*comparisons andThrowIf.Betweenalso have string overloads that take aStringComparison. With a floating-pointNaNoperand,Is.*comparisons returnfalseandThrowIf.*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.Emailwith 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
SGuardCallbackandGuardOutcomefor success/failure handling. - Expression Caching: Selectors are compiled once and cached by expression structure (thread-safe).
- Clear Exception Messages: Built-in exceptions derive from
ArgumentExceptionand 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.
- 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.* fails fast with informative exceptions when rules are violated.
- Choose the style that fits your code:
Culture-aware comparisons and inclusive ranges
- String overloads of the
Is.*comparisons andThrowIf.Betweenaccept StringComparison for correct cultural/ordinal semantics. - Between checks are inclusive by design for predictable validation.
- String overloads of the
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
ArgumentExceptionwhenmin > 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
NaNoperand (double,float,Half) makesIs.*comparisons returnfalseandThrowIf.*comparisons throw. A check such asif (Is.GreaterThan(x, max)) reject();therefore letsNaNthrough; preferThrowIf.*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.
π Links
| Product | Versions 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. |
-
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.