Skip to content

Pragmatic.Ensure

Guard clauses for parameter validation in .NET. Part of the Pragmatic.Design ecosystem.

Every .NET codebase needs to validate inputs. Without a consistent approach, guard clauses become a patchwork of styles:

public class OrderService
{
private readonly IOrderRepository _repository;
private readonly ILogger<OrderService> _logger;
public OrderService(IOrderRepository repository, ILogger<OrderService> logger)
{
// Developer A: manual throw with nameof
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
// Developer B: forgot the guard
_logger = logger;
}
public async Task<Order> GetOrderAsync(Guid id)
{
// Developer C: wrong exception type
if (id == Guid.Empty) throw new InvalidOperationException("Id cannot be empty");
var order = await _repository.GetByIdAsync(id);
// Developer D: exception for a business case
if (order is null) throw new KeyNotFoundException($"Order {id} not found");
return order;
}
}

Four developers, four styles. Inconsistent exception types, missing guards, and exceptions used for business logic that should return typed errors.

The deeper problem: there is no clear boundary between programming errors and business failures. A null constructor argument is a bug — it should throw. A missing order is an expected outcome — it should return a typed error. Mixing these two categories leads to try/catch blocks that catch everything, hiding real bugs behind business error handling.

One static class with three patterns, each designed for a specific category of validation:

public class OrderService(IOrderRepository repository, ILogger<OrderService> logger)
{
// ThrowIf: Guards for programming errors -- validate + assign in one statement
private readonly IOrderRepository _repository = Ensure.ThrowIfNull(repository);
private readonly ILogger<OrderService> _logger = Ensure.ThrowIfNull(logger);
public async Task<Result<Order, NotFoundError>> GetOrderAsync(Guid id)
{
Ensure.ThrowIfEmpty(id); // Programming error: empty GUID is a bug
var order = await _repository.GetByIdAsync(id);
if (order is null)
return new NotFoundError("Order", id); // Business case: typed error, not exception
return order;
}
}

Parameter names are captured automatically. Exception types are always correct. Programming errors throw. Business failures return typed errors.


  • ThrowIf Pattern: Throws exceptions when preconditions fail (for programming errors)
  • Is Pattern: Returns booleans for conditional logic (no exceptions)
  • Check Pattern: Returns VoidResult<TError> for domain validation (expected failures)
  • Zero dependencies: Layer 0 foundation library
  • High performance: Aggressive inlining, no allocations on happy path
  • Automatic parameter names: Uses [CallerArgumentExpression] — no magic strings
  • Null-state analysis: Uses [NotNull] and [NotNullWhen] for proper flow analysis
PackagePurpose
Pragmatic.EnsureCore: ThrowIf* and Is* patterns. Zero dependencies.
Pragmatic.Ensure.ResultCheck.* pattern returning VoidResult<TError>. Depends on Pragmatic.Result.
Terminal window
# Core package (ThrowIf and Is patterns)
dotnet add package Pragmatic.Ensure
# Optional: Result integration (Check pattern)
dotnet add package Pragmatic.Ensure.Result

Use ThrowIf* methods to validate preconditions. They throw exceptions when validation fails:

using Pragmatic.Ensure;
public class User
{
public User(Guid id, string name, string email, int age)
{
// All methods automatically capture parameter names
Ensure.ThrowIfEmpty(id); // ArgumentException
Ensure.ThrowIfNullOrWhiteSpace(name); // ArgumentException
Ensure.ThrowIfNotEmail(email); // ArgumentException
Ensure.ThrowIfNegative(age); // ArgumentOutOfRangeException
Ensure.ThrowIfOutOfRange(age, 0, 150);// ArgumentOutOfRangeException
Id = id;
Name = name;
Email = email;
Age = age;
}
}
// ThrowIfNull returns the validated value for fluent assignment
public class OrderService(IOrderRepository repository, ILogger<OrderService> logger)
{
private readonly IOrderRepository _repository = Ensure.ThrowIfNull(repository);
private readonly ILogger<OrderService> _logger = Ensure.ThrowIfNull(logger);
}

Use Is* methods for validation without exceptions:

using Pragmatic.Ensure;
// Validation without throwing
if (Ensure.IsNotNullOrEmpty(input))
{
// Process valid input
}
// Combine multiple checks
if (Ensure.IsEmail(email) && Ensure.IsLengthInRange(email, 5, 100))
{
// Valid email with proper length
}
// Null-state analysis works correctly
string? name = GetName();
if (Ensure.IsNotNull(name))
{
// 'name' is known to be non-null here
Console.WriteLine(name.Length);
}

Check Pattern (Domain Validation with Result)

Section titled “Check Pattern (Domain Validation with Result)”

Use Check.* methods for domain validation where failures are expected outcomes:

using Pragmatic.Ensure.Result;
// Domain validation returns VoidResult<TError>
var result = Check.NotNull(user, new NotFoundError("User", userId))
.Bind(_ => Check.NotNullOrEmpty(user.Email, new ValidationError("Email required")))
.Bind(_ => Check.Email(user.Email, new ValidationError("Invalid email")));
if (result.IsFailure)
{
return result.Error; // Handle validation failure
}
// Lazy error construction -- factory only invoked on failure
var validation = Check.NotNull(order, () => new NotFoundError("Order", orderId));

QuestionAnswerUse
Is this a programming error (bug in caller)?YesEnsure.ThrowIf*
Do you need a boolean for conditional logic?YesEnsure.Is*
Is failure expected (user input, business rules)?YesCheck.*
AspectThrowIf*Is*Check.*
ReturnsThe validated value (or void)boolVoidResult<TError>
On failureExceptionfalseTyped error
Use forBugs / preconditionsControl flowBusiness validation
ComposableNo&& / ||.Bind() chain
PerformanceZero alloc (happy path)Zero allocZero alloc (happy path)
PackagePragmatic.EnsurePragmatic.EnsurePragmatic.Ensure.Result

public class Reservation
{
public Reservation(Guid guestId, Guid propertyId, DateTimeOffset checkIn, DateTimeOffset checkOut, int guests)
{
Ensure.ThrowIfEmpty(guestId);
Ensure.ThrowIfEmpty(propertyId);
Ensure.ThrowIfInPast(checkIn);
Ensure.ThrowIfTrue(checkOut <= checkIn, "CheckOut must be after CheckIn");
Ensure.ThrowIfOutOfRange(guests, 1, 20);
GuestId = guestId;
PropertyId = propertyId;
CheckIn = checkIn;
CheckOut = checkOut;
NumberOfGuests = guests;
}
}
public async Task<Result<Invoice, IError>> CreateInvoice(CreateInvoiceRequest request)
{
// Programming error: null request is a bug in the caller
Ensure.ThrowIfNull(request);
// Business validation: empty line items is an expected failure
var lineCheck = Check.NotNullOrEmpty(request.LineItems, new ValidationError("At least one line item is required"));
if (lineCheck.IsFailure)
return lineCheck.Error;
// Business validation: negative total is an expected failure
var totalCheck = Check.Positive(request.Total, new ValidationError("Total must be positive"));
if (totalCheck.IsFailure)
return totalCheck.Error;
// ... create invoice
}
public class SmtpSettings
{
public SmtpSettings(string host, int port, string? username, string? password)
{
Host = Ensure.ThrowIfNullOrWhiteSpace(host);
Ensure.ThrowIfOutOfRange(port, 1, 65535);
Port = port;
// Optional fields: only validate format if present
if (Ensure.IsNotNullOrEmpty(username))
{
Username = username;
Password = Ensure.ThrowIfNull(password); // If username is set, password is required
}
}
}

MethodThrowsDescription
ThrowIfNull(value)ArgumentNullExceptionReference types. Returns the validated non-null value
ThrowIfNull(value)ArgumentNullExceptionNullable value types. Returns the unwrapped value
IsNotNull(value)-Returns true if not null
MethodThrowsDescription
ThrowIfNullOrEmpty(value)ArgumentExceptionNull or empty
ThrowIfNullOrWhiteSpace(value)ArgumentExceptionNull, empty, or whitespace
ThrowIfLongerThan(value, max)ArgumentExceptionExceeds max length
ThrowIfShorterThan(value, min)ArgumentExceptionBelow min length
ThrowIfLengthOutOfRange(value, min, max)ArgumentExceptionOutside length range
ThrowIfNotEmail(value)ArgumentExceptionInvalid email format
ThrowIfNotUrl(value, httpsOnly)ArgumentExceptionInvalid URL format
ThrowIfNotPhone(value)ArgumentExceptionInvalid phone format
ThrowIfNotCreditCard(value)ArgumentExceptionInvalid credit card (Luhn)
ThrowIfNotMatch(value, pattern)ArgumentExceptionDoesn’t match regex
IsNotNullOrEmpty(value)-Returns true if valid
IsNotNullOrWhiteSpace(value)-Returns true if valid
IsLengthInRange(value, min, max)-Returns true if in range
IsEmail(value)-Returns true if valid email
IsUrl(value)-Returns true if valid URL
IsPhone(value)-Returns true if valid phone
IsCreditCard(value)-Returns true if valid card
IsMatch(value, pattern)-Returns true if matches
MethodConstraintThrowsDescription
ThrowIfNegative(value)INumber<T>ArgumentOutOfRangeExceptionValue < 0
ThrowIfNegativeOrZero(value)INumber<T>ArgumentOutOfRangeExceptionValue <= 0
ThrowIfZero(value)INumber<T>ArgumentOutOfRangeExceptionValue == 0
ThrowIfPositive(value)INumber<T>ArgumentOutOfRangeExceptionValue > 0
ThrowIfOutOfRange(value, min, max)IComparable<T>ArgumentOutOfRangeExceptionOutside range
ThrowIfGreaterThan(value, max)IComparable<T>ArgumentOutOfRangeExceptionExceeds max
ThrowIfLessThan(value, min)IComparable<T>ArgumentOutOfRangeExceptionBelow min
ThrowIfBelowMin(value, min)IComparable<T>ArgumentOutOfRangeExceptionBelow min
ThrowIfAboveMax(value, max)IComparable<T>ArgumentOutOfRangeExceptionAbove max
IsPositive(value)INumber<T>-Returns true if > 0
IsNegative(value)INumber<T>-Returns true if < 0
IsZero(value)INumber<T>-Returns true if == 0
IsNotZero(value)IComparable<T>-Returns true if != 0
IsNotNegative(value)IComparable<T>-Returns true if >= 0
IsInRange(value, min, max)IComparable<T>-Returns true if in range
IsAtLeastMin(value, min)IComparable<T>-Returns true if >= min
IsAtMostMax(value, max)IComparable<T>-Returns true if <= max
MethodThrowsDescription
ThrowIfEmpty(collection)ArgumentNullException / ArgumentExceptionNull or empty
ThrowIfNullOrEmpty(collection)ArgumentNullException / ArgumentExceptionNull or empty
ThrowIfContainsNull(collection)ArgumentNullException / ArgumentExceptionNull or contains null element
ThrowIfCountGreaterThan(collection, max)ArgumentOutOfRangeExceptionCount exceeds max
ThrowIfCountLessThan(collection, min)ArgumentOutOfRangeExceptionCount below min
ThrowIfCountOutOfRange(collection, min, max)ArgumentOutOfRangeExceptionCount outside range
ThrowIfNotDefined(enumValue)ArgumentOutOfRangeExceptionUndefined enum value
IsNotNullOrEmpty(collection)-Returns true if has items

Optimized overloads for: IEnumerable<T>, ICollection<T>, IList<T>, IReadOnlyCollection<T>, T[]

MethodThrowsDescription
ThrowIfEmpty(guid)ArgumentExceptionGuid.Empty
ThrowIfDefault(dateTime)ArgumentOutOfRangeExceptiondefault(DateTime)
ThrowIfInPast(dateTime)ArgumentOutOfRangeExceptionBefore now
ThrowIfInFuture(dateTime)ArgumentOutOfRangeExceptionAfter now
ThrowIfTrue(condition, message?)ArgumentExceptionCondition is true
ThrowIfFalse(condition, message?)ArgumentExceptionCondition is false
IsNotEmpty(guid)-Returns true if not empty
IsNotDefault(dateTime)-Returns true if not default
IsPast(dateTime)-Returns true if in past
IsFuture(dateTime)-Returns true if in future
AreEqual(a, b)-Returns true if equal
AreNotEqual(a, b)-Returns true if not equal
IsDefined(enumValue)-Returns true if valid enum

Returns VoidResult<TError> — use for domain validation where failures are expected.

MethodDescription
Check.NotNull(value, error)Null check (reference and nullable value types)
Check.NotNullOrEmpty(string, error)String null/empty check
Check.NotNullOrWhiteSpace(string, error)String null/whitespace check
Check.LengthInRange(string, min, max, error)String length validation
Check.Email(value, error)Email format validation
Check.Url(value, error, httpsOnly?)URL format validation
Check.Phone(value, error)Phone format validation
Check.Match(value, pattern, error)Regex pattern match
Check.Positive(value, error)Value > 0
Check.NotNegative(value, error)Value >= 0
Check.NotZero(value, error)Value != 0
Check.InRange(value, min, max, error)Value in range
Check.AtLeast(value, min, error)Value >= min
Check.AtMost(value, max, error)Value <= max
Check.NotNullOrEmpty(collection, error)Collection null/empty
Check.ContainsNoNull(collection, error)Collection contains null element
Check.CountNotGreaterThan(collection, max, error)Count exceeds max
Check.CountNotLessThan(collection, min, error)Count below min
Check.NotEmpty(guid, error)Guid not empty
Check.Defined(enumValue, error)Enum defined
Check.That(condition, error)Custom condition true
Check.Not(condition, error)Custom condition false
Check.InPast(dateTime, error)DateTime in past
Check.InFuture(dateTime, error)DateTime in future
Check.NotDefault(dateTime, error)DateTime not default
Check.Equal(a, b, error)Values equal
Check.NotEqual(a, b, error)Values not equal

All methods support lazy error construction via Func<TError> overload.


Format validation methods (ThrowIfNot*) and ThrowIfLengthOutOfRange are null-safe — they return without throwing when the value is null. This is designed for optional fields where presence is not required but format must be correct when present:

// Pattern: optional field with format validation
string? phone = request.Phone;
Ensure.ThrowIfNotPhone(phone); // null -> OK (field is optional)
// "" -> OK (empty is not invalid format)
// "abc" -> throws ArgumentException
// "+1234567890" -> OK

Presence checks (ThrowIfNullOr*) and explicit-length checks (ThrowIfLongerThan, ThrowIfShorterThan) throw on null:

// Pattern: required field
string name = request.Name;
Ensure.ThrowIfNullOrWhiteSpace(name); // null -> throws
// " " -> throws
// "Alice" -> OK
Method CategoryNull Input Behavior
ThrowIfNullOr* (presence)Throws ArgumentNullException
ThrowIfNot* (format)Returns without throwing (null-safe)
ThrowIfLongerThan / ThrowIfShorterThanThrows ArgumentNullException
ThrowIfLengthOutOfRangeReturns without throwing (null-safe)

All guard methods use [MethodImpl(MethodImplOptions.AggressiveInlining)] for zero overhead on the happy path:

// This compiles to essentially the same IL as a manual null check
private readonly IRepository _repo = Ensure.ThrowIfNull(repository);

The [CallerArgumentExpression] attribute captures parameter names at compile time — no reflection, no string allocation. The string is embedded in the assembly metadata.


ProblemSolution
Inconsistent guard clause stylesOne static class: Ensure
Wrong exception types for guardsArgumentNullException, ArgumentException, ArgumentOutOfRangeException automatically
Manual nameof() on every guard[CallerArgumentExpression] captures parameter names automatically
Exceptions for business failuresCheck.* returns VoidResult<TError>
No boolean checks without throwingEnsure.Is* pattern returns bool
Guards don’t composeCheck.* supports .Bind() chaining
Allocation on happy path[AggressiveInlining] + struct returns
Nullable flow analysis breaks[NotNull] / [NotNullWhen] attributes
Collection validation boilerplateThrowIfEmpty, ThrowIfContainsNull, ThrowIfCountOutOfRange
Optional field format validationThrowIfNot* methods are null-safe by design

Pragmatic.Ensure is a pure runtime library — no source generator, no compile-time diagnostics. Range PRAG0100-0199 is reserved for future analyzers.

All guard methods throw standard .NET exceptions (ArgumentNullException, ArgumentException, ArgumentOutOfRangeException) with automatic parameter name capture via [CallerArgumentExpression].


[Fact]
public void ThrowIfNull_WithNull_ThrowsArgumentNullException()
{
string? value = null;
Assert.Throws<ArgumentNullException>(() => Ensure.ThrowIfNull(value));
}
[Fact]
public void ThrowIfNull_WithValue_ReturnsValue()
{
var result = Ensure.ThrowIfNull("hello");
Assert.Equal("hello", result);
}
[Fact]
public void IsEmail_WithValidEmail_ReturnsTrue()
{
Ensure.IsEmail("user@example.com").Should().BeTrue();
}
[Fact]
public void IsEmail_WithInvalidEmail_ReturnsFalse()
{
Ensure.IsEmail("not-an-email").Should().BeFalse();
}
[Fact]
public void Check_NotNull_WithNull_ReturnsError()
{
string? value = null;
var result = Check.NotNull(value, () => new StringError("required"));
Assert.True(result.IsFailure);
}
[Fact]
public void Check_Positive_WithNegative_ReturnsError()
{
var result = Check.Positive(-5m, new ValidationError("Amount must be positive"));
result.IsFailure.Should().BeTrue();
}

See samples/Pragmatic.Ensure.Samples/ for 5 runnable scenarios: constructor guards (null, empty, range), conditional logic with Is pattern, domain entity invariants, string format + numeric guards (Email, URL, Regex, Range, Positive), and collection/date/enum guards with fluent return pattern.

With ModuleIntegration
Pragmatic.ResultCheck.* returns VoidResult<TError>
Pragmatic.ValidationGuards for preconditions, validation for business rules
  • .NET 10.0 or later

MIT