Lightweight guard clause library for defensive programming and parameter validation. Protect method entry points with a fluent Guard.Against.* API that throws appropriate exceptions on validation failure.
Guardian uses the Guard.Against static property as the entry point. Each method validates its input and returns the validated value on success, enabling assignment chaining.
using Noundry.Guardian;
public class Product
{
public Guid Id { get; }
public string Name { get; }
public decimal Price { get; }
public Product(Guid id, string name, decimal price)
{
Id = Guard.Against.DefaultStruct(id);
Name = Guard.Against.NullOrWhiteSpace(name);
Price = Guard.Against.NegativeOrZero(price);
}
}
Every guard method returns the validated value, enabling direct assignment: Name = Guard.Against.Null(name);
Throws the correct exception type: ArgumentNullException, ArgumentException, or ArgumentOutOfRangeException.
Uses [CallerArgumentExpression] to automatically capture the parameter name in exception messages.
Targets .NET 6/7/8/9/10 and .NET Standard 2.0/2.1 with polyfills for older frameworks.
Validates that a reference type or nullable value type is not null. Throws ArgumentNullException. For nullable structs, returns the unwrapped value.
// Reference type
string name = Guard.Against.Null(inputName);
// Nullable struct - returns int, not int?
int value = Guard.Against.Null(nullableInt);
// With custom message
var obj = Guard.Against.Null(input, message: "Input object is required");
Validates string is not null, empty, or whitespace-only. Throws ArgumentException.
string name = Guard.Against.NullOrWhiteSpace(input);
// Fails for: null, "", " ", "\t", "\n"
// Passes for: "a", " text ", "any content"
Validates string is not null or empty. Whitespace-only strings pass. Throws ArgumentException.
string code = Guard.Against.NullOrEmpty(input);
// Fails for: null, ""
// Passes for: " ", " ", "text"
Validates string matches a regex pattern. Throws ArgumentException.
// Email validation
string email = Guard.Against.InvalidFormat(
input,
@"^[^@\s]+@[^@\s]+\.[^@\s]+$");
// Phone number (10 digits)
string phone = Guard.Against.InvalidFormat(
input,
@"^\d{10}$");
// Account number with custom message
string account = Guard.Against.InvalidFormat(
input,
@"^[A-Z]{2}\d{6}$",
message: "Account must be 2 letters followed by 6 digits");
Validates string length is within bounds. Throws ArgumentOutOfRangeException.
// Username: 3-20 characters
string username = Guard.Against.InvalidLength(input, 3, 20);
// Fixed length (exactly 8 characters)
string code = Guard.Against.InvalidLength(input, 8, 8);
All numeric guards work with any IComparable<T> type: int, long, short, byte, float, double, decimal, and more. They throw ArgumentOutOfRangeException.
| Method | Passes When | Example |
|---|---|---|
| Negative(value) | value >= 0 | 0 passes, -1 fails |
| Zero(value) | value != 0 | 1 passes, 0 fails |
| NegativeOrZero(value) | value > 0 | 1 passes, 0 fails |
| Positive(value) | value <= 0 | 0 passes, 1 fails |
| OutOfRange(value, min, max) | min <= value <= max | 5 in [1,10] passes |
| GreaterThan(value, max) | value <= max | max is upper bound |
| LessThan(value, min) | value >= min | min is lower bound |
| GreaterThanOrEqualTo(value, max) | value < max | strictly less than |
| LessThanOrEqualTo(value, min) | value > min | strictly greater than |
int quantity = Guard.Against.Negative(input);
decimal price = Guard.Against.NegativeOrZero(input);
int divisor = Guard.Against.Zero(input);
int age = Guard.Against.OutOfRange(input, 18, 120);
decimal discount = Guard.Against.OutOfRange(percentage, 0m, 100m);
decimal maxPrice = Guard.Against.GreaterThan(input, 999999.99m);
decimal minAmount = Guard.Against.LessThan(input, 0.01m);
Prevent default/uninitialized values from propagating through your system. Throws ArgumentException.
// Struct defaults: Guid.Empty, default(DateTime), 0
Guid id = Guard.Against.DefaultStruct(inputId);
DateTime date = Guard.Against.DefaultStruct(inputDate);
int value = Guard.Against.DefaultStruct(input);
// Reference type default (null)
string text = Guard.Against.Default(input);
// Convenience: Guard.Against.Default works for both structs and classes
Guid id2 = Guard.Against.Default(inputId); // Same as DefaultStruct
Ensure enum values are actually defined members. Catches invalid casts like (OrderStatus)999. Throws ArgumentException.
public enum OrderStatus { Pending, Processing, Shipped, Delivered }
// Valid
OrderStatus status = Guard.Against.NotInEnum(inputStatus);
// Catches invalid casts
var invalid = (OrderStatus)999;
Guard.Against.NotInEnum(invalid); // Throws ArgumentException
Validates collection is not null and has at least one element. Returns the materialized collection.
var items = Guard.Against.NullOrEmpty(inputList);
var ids = Guard.Against.NullOrEmpty(productIds);
// Works with any IEnumerable<T>
var array = Guard.Against.NullOrEmpty(new[] { 1, 2, 3 });
var list = Guard.Against.NullOrEmpty(new List<string> { "a", "b" });
Validates a value exists in a set of allowed values.
string fruit = Guard.Against.NotOneOf(
input,
new[] { "apple", "banana", "orange" });
string status = Guard.Against.NotOneOf(
input,
new[] { "active", "inactive", "pending" },
message: "Status must be one of: active, inactive, pending");
Use custom boolean conditions for business rules that do not fit into other guard categories. Throws ArgumentException.
// Basic condition
Guard.Against.Condition(age >= 18, nameof(age), "Must be 18 or older");
// Business rule: sufficient funds
Guard.Against.Condition(
balance >= withdrawalAmount,
nameof(withdrawalAmount),
$"Insufficient funds. Available: {balance}, Requested: {withdrawalAmount}");
// Date range validation
Guard.Against.Condition(
startDate < endDate,
"dateRange",
"Start date must be before end date");
// Uniqueness constraint
Guard.Against.Condition(
!existingIds.Contains(newId),
nameof(newId),
"ID already exists");
| Guard Method | Exception Type |
|---|---|
| Null | ArgumentNullException |
| NullOrWhiteSpace | ArgumentException |
| NullOrEmpty (string) | ArgumentException |
| NullOrEmpty (collection) | ArgumentNullException / ArgumentException |
| Default, DefaultStruct | ArgumentException |
| Negative, Zero, NegativeOrZero, Positive | ArgumentOutOfRangeException |
| OutOfRange | ArgumentOutOfRangeException |
| GreaterThan, LessThan, etc. | ArgumentOutOfRangeException |
| InvalidFormat | ArgumentException |
| InvalidLength | ArgumentOutOfRangeException |
| NotInEnum | ArgumentException |
| NotOneOf | ArgumentException |
| Condition | ArgumentException |
public class Product
{
public Guid Id { get; }
public string Name { get; }
public decimal Price { get; }
public int StockQuantity { get; }
public Product(Guid id, string name, decimal price, int stockQuantity)
{
Id = Guard.Against.DefaultStruct(id);
Name = Guard.Against.NullOrWhiteSpace(name);
Name = Guard.Against.InvalidLength(Name, 2, 100);
Price = Guard.Against.NegativeOrZero(price);
StockQuantity = Guard.Against.Negative(stockQuantity);
}
}
public void ProcessOrder(
Guid orderId,
string customerEmail,
decimal amount,
List<Guid> productIds)
{
Guard.Against.DefaultStruct(orderId);
Guard.Against.InvalidFormat(customerEmail, @"^[^@\s]+@[^@\s]+\.[^@\s]+$");
Guard.Against.OutOfRange(amount, 0.01m, 100000m);
Guard.Against.NullOrEmpty(productIds);
// Process order...
}
public class ProductsController : ControllerBase
{
private readonly IProductService _service;
public ProductsController(IProductService service)
{
_service = Guard.Against.Null(service);
}
[HttpGet("{id}")]
public async Task<ActionResult<Product>> GetById(Guid id)
{
try
{
Guard.Against.DefaultStruct(id);
var product = await _service.GetByIdAsync(id);
return product == null ? NotFound() : Ok(product);
}
catch (ArgumentException ex)
{
return BadRequest(ex.Message);
}
}
}
public void Transfer(BankAccount target, decimal amount)
{
Guard.Against.Null(target);
Guard.Against.NegativeOrZero(amount);
Guard.Against.GreaterThan(amount, 1000000m);
Guard.Against.Condition(
amount <= Balance,
nameof(amount),
"Insufficient funds for transfer");
Guard.Against.Condition(
target.Id != Id,
nameof(target),
"Cannot transfer to same account");
Balance -= amount;
target.Balance += amount;
}
public void UpdateStatus(OrderStatus newStatus)
{
Guard.Against.NotInEnum(newStatus);
Guard.Against.Condition(
Status != OrderStatus.Cancelled,
nameof(newStatus),
"Cannot update status of cancelled order");
Status = newStatus;
}
| Pattern | Regex |
|---|---|
| ^[^@\s]+@[^@\s]+\.[^@\s]+$ | |
| Phone (10 digits) | ^\d{10}$ |
| US ZIP Code | ^\d{5}(-\d{4})?$ |
| URL | ^https?://[^\s]+$ |
| Alphanumeric | ^[a-zA-Z0-9]+$ |
| UUID/GUID | ^[0-9a-fA-F]{8}-...-[0-9a-fA-F]{12}$ |
| Date (YYYY-MM-DD) | ^\d{4}-\d{2}-\d{2}$ |
| Hex Color | ^#[0-9a-fA-F]{6}$ |
Choose the right guard method for your scenario:
| Scenario | Guard Method |
|---|---|
| Reference must not be null | Guard.Against.Null(value) |
| String must have content | Guard.Against.NullOrWhiteSpace(value) |
| String must match pattern | Guard.Against.InvalidFormat(value, pattern) |
| String length bounds | Guard.Against.InvalidLength(value, min, max) |
| GUID/DateTime not default | Guard.Against.DefaultStruct(value) |
| Number must be positive | Guard.Against.NegativeOrZero(value) |
| Number within range | Guard.Against.OutOfRange(value, min, max) |
| Collection must have items | Guard.Against.NullOrEmpty(collection) |
| Value in allowed set | Guard.Against.NotOneOf(value, allowed) |
| Enum is defined | Guard.Against.NotInEnum(enumValue) |
| Custom business rule | Guard.Against.Condition(expr, name, msg) |