Guard clauses for argument validation with CallerArgumentExpression for automatic parameter name capture.
The Problem
Standard null checks are verbose and require nameof:
// BAD: Verbose and repetitive
public void Process(string? name, IService? service)
{
_name = name ?? throw new ArgumentNullException(nameof(name));
_service = service ?? throw new ArgumentNullException(nameof(service));
}
The Solution
// GOOD: Clean and expressive
public void Process(string? name, IService? service)
{
_name = Guard.NotNull(name);
_service = Guard.NotNull(service);
}
The parameter name is automatically captured - no nameof required.
Null Validation
NotNull
Validates non-null and returns the value:
// Throws ArgumentNullException if null
var validated = Guard.NotNull(possiblyNullValue);
// Can be used inline
ProcessItem(Guard.NotNull(item));
// In constructors
public MyClass(ILogger? logger, IConfig? config)
{
_logger = Guard.NotNull(logger);
_config = Guard.NotNull(config);
}
NotNullOrElse
Returns value or default (no exception):
// Reference types
var name = Guard.NotNullOrElse(user?.Name, "Anonymous");
var service = Guard.NotNullOrElse(optionalService, DefaultService.Instance);
// Value types
int count = Guard.NotNullOrElse(maybeCount, 0);
// Lazy evaluation for expensive defaults
var config = Guard.NotNullOrElse(optionalConfig, () => LoadDefaultConfig());
Member Validation
Validate an object and its member in one call:
// Validates both config and config.ConnectionString are non-null
public void Configure(Config? config)
{
var connectionString = Guard.NotNullWithMember(config, config?.ConnectionString);
// Throws ArgumentNullException if config is null
// Throws ArgumentException if config.ConnectionString is null
}
// When the parent is known non-null, validate only the member
public void Process(Config config)
{
var timeout = Guard.MemberNotNull(config, config.Timeout);
// Throws ArgumentException if config.Timeout is null
}
String Validation
NotNullOrEmpty
Validates non-null and non-empty strings:
public void SetName(string? name)
{
_name = Guard.NotNullOrEmpty(name);
// Throws ArgumentNullException if null
// Throws ArgumentException if empty
}
NotNullOrWhiteSpace
Validates strings contain meaningful content:
public void SetDescription(string? description)
{
_description = Guard.NotNullOrWhiteSpace(description);
// Throws if null, empty, or only whitespace
}
With Defaults
// Return default if null/empty
var displayName = Guard.NotNullOrEmptyOrElse(user.DisplayName, user.Username);
// Return default if null/empty/whitespace
var title = Guard.NotNullOrWhiteSpaceOrElse(article.Title, "Untitled");
// Lazy evaluation for expensive defaults
var config = Guard.NotNullOrEmptyOrElse(optionalConfig, () => LoadDefaultConfig());
String Length Validation
HasLength
Validates exact string length:
public void SetCountryCode(string? code)
{
_code = Guard.HasLength(code, 2); // ISO country codes
// Throws ArgumentNullException if null
// Throws ArgumentException if length != 2
}
HasMinLength / HasMaxLength
Validates minimum or maximum string length:
public void SetPassword(string? password)
{
_password = Guard.HasMinLength(password, 8);
// Throws if length < 8
}
public void SetUsername(string? username)
{
_username = Guard.HasMaxLength(username, 50);
// Throws if length > 50
}
HasLengthBetween
Validates string length is within a range (inclusive):
public void SetDisplayName(string? name)
{
_name = Guard.HasLengthBetween(name, 3, 100);
// Throws if length < 3 or length > 100
}
Collection Validation
NotNullOrEmpty
public void ProcessItems(IReadOnlyCollection<Item>? items)
{
var validItems = Guard.NotNullOrEmpty(items);
// Throws ArgumentNullException if null
// Throws ArgumentException if empty
foreach (var item in validItems)
{
// Process items
}
}
NoDuplicates
Validates that a collection contains no duplicate elements:
public void SetIds(IEnumerable<int> ids)
{
Guard.NoDuplicates(ids);
// Throws ArgumentException with "Duplicate value found: {value}"
}
// IReadOnlyList overload returns the validated list
public void Process(IReadOnlyList<string>? names)
{
var validNames = Guard.NoDuplicates(names);
// Validates non-null AND no duplicates
}
// With custom equality comparer
public void SetNames(IEnumerable<string> names)
{
Guard.NoDuplicates(names, StringComparer.OrdinalIgnoreCase);
// Case-insensitive duplicate detection
}
Value Type Validation
NotDefault
Validates that a value type is not its default value:
public void SetId(Guid id)
{
_id = Guard.NotDefault(id);
// Throws ArgumentException if id == default(Guid)
}
public void SetTimestamp(DateTime timestamp)
{
_timestamp = Guard.NotDefault(timestamp);
// Throws if timestamp == default(DateTime)
}
NotEmpty (Guid)
Validates that a Guid is not empty:
public void SetUserId(Guid userId)
{
_userId = Guard.NotEmpty(userId);
// Throws ArgumentException if userId == Guid.Empty
}
NotEmpty for Guid is semantically clearer than NotDefault when working
with identifiers, though they produce the same result for Guid.
Set Membership Validation
OneOf
Validates that a value is one of the allowed values:
public void SetProtocol(string protocol)
{
_protocol = Guard.OneOf(protocol, new[] { "http", "https", "ftp" });
// Throws ArgumentException if not in allowed set
}
// O(1) lookup with HashSet for large allowed sets
private static readonly HashSet<string> AllowedContentTypes = new(StringComparer.OrdinalIgnoreCase)
{
"application/json",
"application/xml",
"text/plain"
};
public void SetContentType(string contentType)
{
_contentType = Guard.OneOf(contentType, AllowedContentTypes);
}
NotOneOf
Validates that a value is not one of the disallowed values:
public void SetPort(int port)
{
_port = Guard.NotOneOf(port, new[] { 0, 80, 443 }); // Reserved ports
// Throws ArgumentException if in disallowed set
}
// O(1) lookup with HashSet
private static readonly HashSet<int> ReservedPorts = new() { 0, 80, 443, 8080 };
public void SetCustomPort(int port)
{
_port = Guard.NotOneOf(port, ReservedPorts);
}
Range Validation
InRange
Validates value is within bounds (inclusive):
public void SetPage(int page)
{
_page = Guard.InRange(page, 1, 100);
// Throws ArgumentOutOfRangeException if outside range
}
// Works with any IComparable<T>
var temperature = Guard.InRange(temp, -40.0, 100.0);
var date = Guard.InRange(eventDate, DateTime.Today, DateTime.Today.AddYears(1));
ValidIndex
Validates array/list index:
public T GetItem(int index)
{
Guard.ValidIndex(index, _items.Count);
return _items[index];
}
Numeric Validation
All numeric validation methods use [MethodImpl(AggressiveInlining)] for hot paths and are available for int, long, double, and decimal types.
Basic Constraints
// Value cannot be zero
var divisor = Guard.NotZero(value);
// Value must be >= 0
var count = Guard.NotNegative(value);
// Value must be > 0
var quantity = Guard.Positive(value);
Boundary Constraints
// Upper bound (inclusive): value <= max
var percentage = Guard.NotGreaterThan(value, 100);
// Lower bound (inclusive): value >= min
var temperature = Guard.NotLessThan(value, -273.15);
// Upper bound (exclusive): value < max
var index = Guard.LessThan(value, array.Length);
// Lower bound (exclusive): value > min
var priority = Guard.GreaterThan(value, 0);
Double-Specific Validation
// Validates value is not NaN
var result = Guard.NotNaN(calculatedValue);
// Validates value is finite (not NaN or Infinity)
var coefficient = Guard.Finite(inputValue);
Double comparison methods (NotNegative, Positive, etc.) correctly handle
NaN values by throwing ArgumentOutOfRangeException - NaN is not a valid
number.
File System Validation
FileExists / DirectoryExists
Validates that a file or directory exists at the specified path:
public void LoadConfig(string? path)
{
var validPath = Guard.FileExists(path);
// Throws ArgumentNullException if null
// Throws ArgumentException if empty or file doesn't exist
var content = File.ReadAllText(validPath);
}
public void ProcessDirectory(string? path)
{
var validPath = Guard.DirectoryExists(path);
foreach (var file in Directory.GetFiles(validPath)) { }
}
Path Character Validation
// Validates filename contains no invalid characters
var fileName = Guard.ValidFileName(name);
// Throws if name contains characters like: \ / : * ? " < > |
// Nullable variant - returns null if input is null
var optionalName = Guard.ValidFileNameOrNull(name);
// Validates path contains no invalid characters
var validPath = Guard.ValidPath(userInput);
// Nullable variant
var optionalPath = Guard.ValidPathOrNull(userInput);
Extension Validation
// ValidExtension requires NO leading dot
var ext = Guard.ValidExtension("txt"); // Returns "txt"
Guard.ValidExtension(".txt"); // Throws - no leading dot allowed
Guard.ValidExtension("path/txt"); // Throws - no separators allowed
// NormalizedExtension accepts both and ensures leading dot
var ext1 = Guard.NormalizedExtension("txt"); // Returns ".txt"
var ext2 = Guard.NormalizedExtension(".txt"); // Returns ".txt"
Type Validation
DefinedEnum
Validates that an enum value is defined:
public void SetStatus(Status status)
{
_status = Guard.DefinedEnum(status);
// Throws ArgumentOutOfRangeException for undefined values like (Status)999
}
Type Constraints
// Validates type is not Nullable<T>
public void Register(Type type)
{
Guard.NotNullableType(type);
// Throws if type is int?, bool?, etc.
}
// Validates type implements/inherits from T
public void RegisterService(Type implementationType)
{
Guard.AssignableTo<IService>(implementationType);
// Throws if implementationType doesn't implement IService
}
// Validates T can be assigned to type
public void RegisterHandler(Type handlerType)
{
Guard.AssignableFrom<BaseHandler>(handlerType);
// Throws if BaseHandler isn't assignable to handlerType
}
Condition Validation
That
Validates a boolean condition:
public void SetAge(int age)
{
Guard.That(age >= 0, "Age cannot be negative.");
_age = age;
}
Satisfies
Validates value matches a predicate:
public void SetEmail(string email)
{
_email = Guard.Satisfies(email, e => e.Contains('@'), "Invalid email format.");
}
// Complex validation
_config = Guard.Satisfies(config, c =>
c.Timeout > TimeSpan.Zero && c.MaxRetries >= 0,
"Invalid configuration values.");
Unreachable Code
Mark code paths that should never execute:
// In statement context
switch (status)
{
case Status.Active: return "Active";
case Status.Inactive: return "Inactive";
default: Guard.Unreachable();
}
// In expression context (switch expressions)
return status switch
{
Status.Active => "Active",
Status.Inactive => "Inactive",
_ => Guard.Unreachable<string>()
};
// With custom message
Guard.Unreachable("All enum values should be handled");
UnreachableIf
Throws only when a condition is true, useful for asserting invariants:
// Assert an invariant that should never be violated
Guard.UnreachableIf(list.Count < 0, "Count should never be negative");
// In validation logic
Guard.UnreachableIf(
state == ProcessState.Running && !hasStarted,
"Cannot be running without having started");
Unreachable and UnreachableIf automatically capture caller info
(CallerMemberName, CallerFilePath, CallerLineNumber) to provide detailed
exception messages for debugging.
Examples
Constructor Validation
public class OrderProcessor
{
private readonly IOrderRepository _repository;
private readonly IPaymentService _payment;
private readonly ILogger _logger;
public OrderProcessor(
IOrderRepository? repository,
IPaymentService? payment,
ILogger? logger)
{
_repository = Guard.NotNull(repository);
_payment = Guard.NotNull(payment);
_logger = Guard.NotNull(logger);
}
}
Method Parameter Validation
public async Task<Order> CreateOrderAsync(
string? customerId,
IReadOnlyCollection<OrderItem>? items,
decimal discount)
{
var validCustomerId = Guard.NotNullOrWhiteSpace(customerId);
var validItems = Guard.NotNullOrEmpty(items);
var validDiscount = Guard.InRange(discount, 0m, 100m);
// All parameters validated
return await ProcessOrder(validCustomerId, validItems, validDiscount);
}
Configuration Validation
public class ServerConfig
{
public int Port { get; }
public string Host { get; }
public TimeSpan Timeout { get; }
public ServerConfig(int port, string? host, TimeSpan timeout)
{
Port = Guard.InRange(port, 1, 65535);
Host = Guard.NotNullOrWhiteSpace(host);
Timeout = Guard.Satisfies(timeout, t => t > TimeSpan.Zero, "Timeout must be positive.");
}
}
File Processing
public void ProcessFile(string? inputPath, string? outputFileName)
{
// Validate input file exists
var input = Guard.FileExists(inputPath);
// Validate output filename is valid
var output = Guard.ValidFileName(outputFileName);
// Normalize the extension
var ext = Guard.NormalizedExtension("txt"); // Returns ".txt"
// Process...
}
// Optional filename validation (returns null if input is null)
public void SaveFile(string? fileName)
{
var validName = Guard.ValidFileNameOrNull(fileName);
if (validName is not null)
{
// Save with validated name
}
}
API Reference
Null Validation
| Method | Throws | Description |
|---|
NotNull<T>(value) | ArgumentNullException | Validates non-null |
NotNullOrElse<T>(value, default) | - | Returns value or default |
NotNullOrElse<T>(value, Func<T>) | - | Lazy default evaluation |
NotNullWithMember<TParam, TMember>(obj, member) | ArgumentNullException, ArgumentException | Validates both object and member |
MemberNotNull<TParam, TMember>(obj, member) | ArgumentException | Validates member of non-null object |
String Validation
| Method | Throws | Description |
|---|
NotNullOrEmpty(string) | ArgumentNullException, ArgumentException | Validates non-null, non-empty |
NotNullOrWhiteSpace(string) | ArgumentNullException, ArgumentException | Validates has content |
NotNullOrEmptyOrElse(string, default) | - | Returns value or default |
NotNullOrEmptyOrElse(string, Func<string>) | - | Lazy default evaluation |
NotNullOrWhiteSpaceOrElse(string, default) | - | Returns value or default |
NotNullOrWhiteSpaceOrElse(string, Func<string>) | - | Lazy default evaluation |
HasLength(string, int) | ArgumentNullException, ArgumentException | Exact length |
HasMinLength(string, int) | ArgumentNullException, ArgumentException | Minimum length |
HasMaxLength(string, int) | ArgumentNullException, ArgumentException | Maximum length |
HasLengthBetween(string, int, int) | ArgumentNullException, ArgumentException | Length in range |
Collection Validation
| Method | Throws | Description |
|---|
NotNullOrEmpty<T>(collection) | ArgumentNullException, ArgumentException | Validates collection |
NoDuplicates<T>(IEnumerable<T>) | ArgumentException | Validates no duplicates |
NoDuplicates<T>(IEnumerable<T>, comparer) | ArgumentException | With custom equality |
NoDuplicates<T>(IReadOnlyList<T>) | ArgumentNullException, ArgumentException | Validates and returns list |
NoDuplicates<T>(IReadOnlyList<T>, comparer) | ArgumentNullException, ArgumentException | With custom equality |
Value Type Validation
| Method | Throws | Description |
|---|
NotDefault<T>(T) | ArgumentException | Value type not default |
NotEmpty(Guid) | ArgumentException | Guid is not Guid.Empty |
Set Membership Validation
| Method | Throws | Description |
|---|
OneOf<T>(T, T[]) | ArgumentException | Value in allowed set |
OneOf<T>(T, HashSet<T>) | ArgumentException | O(1) lookup variant |
NotOneOf<T>(T, T[]) | ArgumentException | Value not in disallowed set |
NotOneOf<T>(T, HashSet<T>) | ArgumentException | O(1) lookup variant |
File System Validation
| Method | Throws | Description |
|---|
FileExists(path) | ArgumentNullException, ArgumentException | Validates file exists |
DirectoryExists(path) | ArgumentNullException, ArgumentException | Validates directory exists |
ValidFileName(name) | ArgumentNullException, ArgumentException | No invalid filename chars |
ValidFileNameOrNull(name) | ArgumentException | Nullable variant |
ValidPath(path) | ArgumentNullException, ArgumentException | No invalid path chars |
ValidPathOrNull(path) | ArgumentException | Nullable variant |
ValidExtension(ext) | ArgumentNullException, ArgumentException | No leading dot, no separators |
NormalizedExtension(ext) | ArgumentNullException, ArgumentException | Ensures leading dot |
Type Validation
| Method | Throws | Description |
|---|
DefinedEnum<T>(value) | ArgumentOutOfRangeException | Validates enum is defined |
NotNullableType(type) | ArgumentNullException, ArgumentException | Not Nullable<T> |
AssignableTo<T>(type) | ArgumentNullException, ArgumentException | Type implements T |
AssignableFrom<T>(type) | ArgumentNullException, ArgumentException | T assignable to type |
Numeric Validation (int, long, double, decimal)
| Method | Throws | Description |
|---|
NotZero(value) | ArgumentOutOfRangeException | Value != 0 |
NotNegative(value) | ArgumentOutOfRangeException | Value >= 0 |
Positive(value) | ArgumentOutOfRangeException | Value > 0 |
NotGreaterThan(value, max) | ArgumentOutOfRangeException | value ≤ max |
NotLessThan(value, min) | ArgumentOutOfRangeException | value ≥ min |
LessThan(value, max) | ArgumentOutOfRangeException | value < max |
GreaterThan(value, min) | ArgumentOutOfRangeException | value > min |
InRange<T>(value, min, max) | ArgumentOutOfRangeException | min ≤ value ≤ max |
ValidIndex(index, count) | ArgumentOutOfRangeException | 0 ≤ index < count |
Double-Specific
| Method | Throws | Description |
|---|
NotNaN(value) | ArgumentOutOfRangeException | Value is not NaN |
Finite(value) | ArgumentOutOfRangeException | Not NaN or Infinity |
Condition Validation
| Method | Throws | Description |
|---|
That(condition, message) | ArgumentException | Validates condition |
Satisfies<T>(value, predicate, message) | ArgumentException | Validates with predicate |
Unreachable(message?) | InvalidOperationException | Marks unreachable code |
Unreachable<T>(message?) | InvalidOperationException | For expression contexts |
UnreachableIf(condition, message?) | InvalidOperationException | Throws only when condition true |
All validation methods use CallerArgumentExpressionAttribute to
automatically capture the parameter name in exception messages - no nameof()
required.