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.
- 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.
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.
dotnet add package 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.
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.
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;
}
}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)// 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")));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."));// 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)- 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).
- 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.
// 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.
// 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")));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)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);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...
}Join our community chat to ask questions, share feedback, or get involved: #sguard:gitter.im
| Total | Passed | Failed | Skipped |
|---|---|---|---|
| 367 | 367 | 0 | 0 |
| 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% |
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
This project adheres to the .NET Foundation Code of Conduct. By participating, you are expected to uphold this code.
This project is licensed under the MIT License, a permissive open source license. See the LICENSE file for details.