Skip to content

Troubleshooting

Practical problem/solution guide for Pragmatic.Ensure. Each section covers a common issue, the likely causes, and the fix.


The exception message shows the wrong parameter name (e.g., (Parameter 'value') instead of the actual variable name).

  1. Are you passing a literal or intermediate variable? [CallerArgumentExpression] captures the exact expression at the call site. If you extract the value into a local variable first, the local name is captured:

    var val = GetEmail();
    Ensure.ThrowIfNullOrWhiteSpace(val);
    // Message: (Parameter 'val') — not the original source

    Pass the expression directly for the best parameter name:

    Ensure.ThrowIfNullOrWhiteSpace(GetEmail());
    // Message: (Parameter 'GetEmail()')

    Or use a descriptive local:

    var customerEmail = GetEmail();
    Ensure.ThrowIfNullOrWhiteSpace(customerEmail);
    // Message: (Parameter 'customerEmail')
  2. Are you passing an explicit paramName? The paramName parameter is optional. If you pass it explicitly, it overrides the auto-captured expression:

    Ensure.ThrowIfNull(value, "customName");
    // Message: (Parameter 'customName')
  3. Are you on an older .NET version? [CallerArgumentExpression] requires C# 10 / .NET 6 or later at the compiler level. If your toolchain is older, the parameter defaults to null and the exception message will not include a parameter name.


Null Value Passes Through Format Validation

Section titled “Null Value Passes Through Format Validation”

You call ThrowIfNotEmail(value) with a null value, but no exception is thrown.

Format validation methods are null-safe by design. They return without throwing when the value is null. This is intentional for optional fields.

Null-safe methods (return silently on null)

Section titled “Null-safe methods (return silently on null)”
  • ThrowIfNotEmail
  • ThrowIfNotUrl
  • ThrowIfNotPhone
  • ThrowIfNotCreditCard
  • ThrowIfNotMatch
  • ThrowIfLengthOutOfRange

If the field is required, validate presence first:

Ensure.ThrowIfNullOrWhiteSpace(email); // Throws on null
Ensure.ThrowIfNotEmail(email); // Now email is guaranteed non-null

If the field is optional, the null-safe behavior is correct — null means “no value provided,” which is valid.


Check.* Method Returns Success When Value Is Null

Section titled “Check.* Method Returns Success When Value Is Null”

Check.Email(nullValue, error) returns success instead of the expected error.

Check.Email, Check.Url, Check.Phone, Check.Match, and Check.LengthInRange delegate to the corresponding Ensure.Is* methods, which have null-safe semantics. IsEmail(null) returns false, so Check.Email(null, error) returns failure. However, IsMatch(null, pattern) returns true (null is considered valid for regex matching), so Check.Match(null, pattern, error) returns success.

If you need to reject null values, add an explicit null check before the format check:

var result = Check.NotNull(value, new ValidationError("Field is required"))
.Bind(_ => Check.Match(value, @"^\d{5}$", new ValidationError("Must be 5 digits")));

Collection Guard Throws ArgumentNullException Instead of ArgumentException

Section titled “Collection Guard Throws ArgumentNullException Instead of ArgumentException”

You pass an empty collection to ThrowIfNullOrEmpty, expecting ArgumentException, but get ArgumentNullException.

The collection is actually null, not empty. All ThrowIf* collection methods check null first and throw ArgumentNullException before checking emptiness. An empty collection produces ArgumentException.

If you need to distinguish between null and empty in error handling, check them separately:

Ensure.ThrowIfNull(items); // ArgumentNullException if null
// At this point, items is guaranteed non-null
if (items.Count == 0)
{
// Handle empty case differently if needed
}

In most cases, ThrowIfNullOrEmpty is the right single call — null and empty are both invalid for a required collection.


IEnumerable Collection Check Enumerates the Sequence

Section titled “IEnumerable Collection Check Enumerates the Sequence”

After calling Ensure.ThrowIfNullOrEmpty(query) where query is an IEnumerable<T> from a LINQ chain, the sequence behaves unexpectedly (e.g., database query runs twice).

The IEnumerable<T> overload of ThrowIfNullOrEmpty calls .Any(), which enumerates the first element. If the enumerable is a deferred LINQ query (e.g., an EF Core IQueryable), this triggers execution.

Materialize the collection first, or use a more specific overload:

// Option A: Materialize first
var items = query.ToList();
Ensure.ThrowIfNullOrEmpty(items); // Uses ICollection<T> overload — no enumeration
// Option B: Use IReadOnlyCollection<T> or T[] overloads (O(1) count check)
IReadOnlyList<Order> orders = await query.ToListAsync(ct);
Ensure.ThrowIfNullOrEmpty(orders); // Uses IReadOnlyCollection<T> overload

The compiler selects the most specific overload automatically. List<T> implements ICollection<T>, so passing a List<T> uses the Count property (O(1)), not .Any().


Numeric Guard Does Not Accept My Custom Type

Section titled “Numeric Guard Does Not Accept My Custom Type”

Ensure.ThrowIfNegative(myMoney) does not compile because Money does not implement INumber<T>.

ThrowIfNegative, ThrowIfNegativeOrZero, ThrowIfZero, and ThrowIfPositive require INumber<T>. ThrowIfOutOfRange, ThrowIfGreaterThan, ThrowIfLessThan, ThrowIfBelowMin, and ThrowIfAboveMax require IComparable<T>.

If your type implements IComparable<T> but not INumber<T>, use the range methods:

// Money implements IComparable<Money>
Ensure.ThrowIfLessThan(amount, Money.Zero); // Works with IComparable<T>

If your type implements neither, extract the underlying value:

Ensure.ThrowIfNegative(amount.Value); // Use the decimal/int inside

ThrowIfNotMatch or IsMatch returns unexpected results, and the ActivitySource shows a timeout event.

All regex-based validations have a 250ms timeout to prevent ReDoS attacks. If the input triggers catastrophic backtracking, the regex engine times out.

PatternBehavior
ThrowIfNotMatchThrows ArgumentException (value treated as invalid)
IsMatchReturns false
Check.MatchReturns failure
  1. Simplify the regex. Avoid nested quantifiers ((a+)+), alternation with overlap ((a|a+)), and other ReDoS-prone patterns.

  2. Consider using Is* methods. If the value might legitimately be long (e.g., user-provided text), use IsMatch and handle false gracefully instead of relying on ThrowIfNotMatch.

  3. Validate input length first. Limit the input size before running the regex:

    Ensure.ThrowIfLongerThan(value, 500);
    Ensure.ThrowIfNotMatch(value, complexPattern);

Timeout events are emitted via ActivitySource("Pragmatic.Ensure") with tags:

  • ensure.validation_type — the type of validation ("custom_pattern" for ThrowIfNotMatch, "phone" for phone)
  • ensure.input_length — the length of the input string
  • ensure.timeout_ms — always 250

Configure your OpenTelemetry exporter to capture these activities for monitoring.


You chain multiple Check.* calls with .Bind(), but only the first error is returned even though multiple fields are invalid.

.Bind() short-circuits: when the first check fails, subsequent checks are not executed. This is by design — Bind is monadic sequencing, not parallel validation.

If you need to collect all validation errors at once, use Pragmatic.Validation with ISyncValidator<T> or IAsyncValidator<T> instead of Check.* chains. The Validation module is designed for collecting multiple field errors.

For sequential validation where you want the first error:

// First failure stops the chain — this is correct behavior for Check
var result = Check.NotNullOrWhiteSpace(dto.Name, nameError)
.Bind(_ => Check.Email(dto.Email, emailError))
.Bind(_ => Check.InRange(dto.Age, 18, 120, ageError));

Pragmatic.Ensure has a reserved diagnostic range of PRAG0100-PRAG0199. Currently, there are no compile-time diagnostics emitted by the source generator for the Ensure module — all validation is purely runtime.

The runtime guard methods produce standard .NET exceptions:

ExceptionThrown by
ArgumentNullExceptionThrowIfNull, collection methods when collection is null
ArgumentExceptionThrowIfNullOrEmpty (string), ThrowIfNullOrWhiteSpace, ThrowIfNotEmail, ThrowIfNotUrl, ThrowIfNotPhone, ThrowIfNotCreditCard, ThrowIfNotMatch, ThrowIfEmpty (Guid), ThrowIfContainsNull, ThrowIfEmpty (collection), ThrowIfTrue, ThrowIfFalse
ArgumentOutOfRangeExceptionThrowIfNegative, ThrowIfNegativeOrZero, ThrowIfZero, ThrowIfPositive, ThrowIfOutOfRange, ThrowIfGreaterThan, ThrowIfLessThan, ThrowIfBelowMin, ThrowIfAboveMax, ThrowIfDefault, ThrowIfInPast, ThrowIfInFuture, ThrowIfNotDefined, ThrowIfCountGreaterThan, ThrowIfCountLessThan, ThrowIfCountOutOfRange

Do I need both Pragmatic.Ensure and Pragmatic.Ensure.Result?

Section titled “Do I need both Pragmatic.Ensure and Pragmatic.Ensure.Result?”

No. Install only what you need:

  • Pragmatic.EnsureThrowIf* and Is* methods, zero dependencies
  • Pragmatic.Ensure.ResultCheck.* methods, depends on Pragmatic.Result

If you only use guard clauses and boolean checks, the core package is sufficient.

Can I use Ensure in a netstandard2.0 library?

Section titled “Can I use Ensure in a netstandard2.0 library?”

No. Pragmatic.Ensure targets .NET 10. The numeric methods use INumber<T> (introduced in .NET 7), and [CallerArgumentExpression] requires C# 10. If you need to target older frameworks, you cannot use this package.

By design. The three patterns have distinct failure semantics: ThrowIf* throws, Is* returns bool, Check.* returns VoidResult<TError>. If you want to throw on validation failure, use ThrowIf*. If you want a boolean for branching, use Is*.

Why does ThrowIfNotEmail accept null without throwing?

Section titled “Why does ThrowIfNotEmail accept null without throwing?”

Format validation methods are null-safe for optional fields. If the field is required, add ThrowIfNullOrWhiteSpace before the format check. See Common Mistakes #6 for the full explanation.

Ensure is a static class and cannot be extended via inheritance. Create your own static class with domain-specific guards:

public static class DomainEnsure
{
public static void ThrowIfInvalidCurrency(
string code,
[CallerArgumentExpression(nameof(code))] string? paramName = null)
{
Ensure.ThrowIfNullOrWhiteSpace(code);
if (code.Length != 3)
throw new ArgumentException("Currency code must be 3 characters.", paramName);
}
}

What is the performance cost of guard clauses?

Section titled “What is the performance cost of guard clauses?”

On the happy path, zero allocations and a single branch instruction (the method is inlined by the JIT). See the Performance Characteristics table in the Concepts guide.