SGuard 0.1.0
See the version list below for details.
dotnet add package SGuard --version 0.1.0
NuGet\Install-Package SGuard -Version 0.1.0
<PackageReference Include="SGuard" Version="0.1.0" />
<PackageVersion Include="SGuard" Version="0.1.0" />
<PackageReference Include="SGuard" />
paket add SGuard --version 0.1.0
#r "nuget: SGuard, 0.1.0"
#:package SGuard@0.1.0
#addin nuget:?package=SGuard&version=0.1.0
#tool nuget:?package=SGuard&version=0.1.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.
π Whatβs New in 0.1.0
- Versioning reset: starting fresh at
0.1.0
. Older NuGet versions have been unlisted/removed.- No functional breaking changes are expected for consumers adopting this version.
- Targets: .NET 6, 7, 8, and 9.
- Packaging: README, LICENSE, and package icon included in the NuGet package.
π 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
,GreaterThanOrEqual
for 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
SGuardCallback
andGuardOutcome
for 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.
π¦ 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.* 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/04/2025 - 20:34:01 |
Coverage date: | 09/03/2025 - 19:56:53 - 09/04/2025 - 20:33:59 |
Parser: | MultiReport (11x 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
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last Updated |
---|---|---|
0.1.1 | 61 | 9/5/2025 |
# Release Notes - Version 0.1.0
This release restarts the package versioning at 0.1.0. Previous NuGet versions have been unlisted/removed.
The codebase remains functionally compatible with the current API surface; consumers adopting SGuard from
this version should not require code changes.
## Highlights
- Fresh start at v0.1.0 under the **MIT License**
- Targets: **.NET 6 / 7 / 8 / 9**
- README, LICENSE, and package icon included in the NuGet package
## Features
- Guard APIs:
- Boolean checks via `Is.*`
- Throwing guards via `ThrowIf.*` with CallerArgumentExpression for rich diagnostics
- Unified model with `GuardCallback` and `GuardOutcome` for consistent outcomes and callbacks
- Comparisons:
- Generic comparisons for `IComparable<T>`
- String operations with `StringComparison` support and culture-aware scenarios
- Range helpers (Between, LessThan, LessThanOrEqual, etc.)
- Collections and predicates:
- `Any<T>` and `All<T>` with predicate support
- Null/empty checks with selector support and expression caching for performance
- Exceptions:
- Multiple overloads for custom exception types
- Parameterless and parameterized constructors supported
- Generic creation via `ExceptionActivator.Create<T>`
## Notes
- If you were using earlier package versions, please migrate to `0.1.0`. No runtime breaking changes are expected.