Troubleshooting
Practical problem/solution guide for Pragmatic.Ensure. Each section covers a common issue, the likely causes, and the fix.
Guard Throws but Parameter Name Is Wrong
Section titled “Guard Throws but Parameter Name Is Wrong”The exception message shows the wrong parameter name (e.g., (Parameter 'value') instead of the actual variable name).
Checklist
Section titled “Checklist”-
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 sourcePass 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') -
Are you passing an explicit
paramName? TheparamNameparameter is optional. If you pass it explicitly, it overrides the auto-captured expression:Ensure.ThrowIfNull(value, "customName");// Message: (Parameter 'customName') -
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 tonulland 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)”ThrowIfNotEmailThrowIfNotUrlThrowIfNotPhoneThrowIfNotCreditCardThrowIfNotMatchThrowIfLengthOutOfRange
If the field is required, validate presence first:
Ensure.ThrowIfNullOrWhiteSpace(email); // Throws on nullEnsure.ThrowIfNotEmail(email); // Now email is guaranteed non-nullIf 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-nullif (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 firstvar 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> overloadThe 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 insideRegex Validation Times Out
Section titled “Regex Validation Times Out”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.
Behavior on timeout
Section titled “Behavior on timeout”| Pattern | Behavior |
|---|---|
ThrowIfNotMatch | Throws ArgumentException (value treated as invalid) |
IsMatch | Returns false |
Check.Match | Returns failure |
-
Simplify the regex. Avoid nested quantifiers (
(a+)+), alternation with overlap ((a|a+)), and other ReDoS-prone patterns. -
Consider using
Is*methods. If the value might legitimately be long (e.g., user-provided text), useIsMatchand handlefalsegracefully instead of relying onThrowIfNotMatch. -
Validate input length first. Limit the input size before running the regex:
Ensure.ThrowIfLongerThan(value, 500);Ensure.ThrowIfNotMatch(value, complexPattern);
Observability
Section titled “Observability”Timeout events are emitted via ActivitySource("Pragmatic.Ensure") with tags:
ensure.validation_type— the type of validation ("custom_pattern"forThrowIfNotMatch,"phone"for phone)ensure.input_length— the length of the input stringensure.timeout_ms— always250
Configure your OpenTelemetry exporter to capture these activities for monitoring.
VoidResult Chain Stops at First Failure
Section titled “VoidResult Chain Stops at First Failure”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 Checkvar result = Check.NotNullOrWhiteSpace(dto.Name, nameError) .Bind(_ => Check.Email(dto.Email, emailError)) .Bind(_ => Check.InRange(dto.Age, 18, 120, ageError));Diagnostics Reference
Section titled “Diagnostics Reference”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:
| Exception | Thrown by |
|---|---|
ArgumentNullException | ThrowIfNull, collection methods when collection is null |
ArgumentException | ThrowIfNullOrEmpty (string), ThrowIfNullOrWhiteSpace, ThrowIfNotEmail, ThrowIfNotUrl, ThrowIfNotPhone, ThrowIfNotCreditCard, ThrowIfNotMatch, ThrowIfEmpty (Guid), ThrowIfContainsNull, ThrowIfEmpty (collection), ThrowIfTrue, ThrowIfFalse |
ArgumentOutOfRangeException | ThrowIfNegative, 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.Ensure—ThrowIf*andIs*methods, zero dependenciesPragmatic.Ensure.Result—Check.*methods, depends onPragmatic.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.
Why do Is* methods not throw?
Section titled “Why do Is* methods not throw?”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.
Can I extend Ensure with custom methods?
Section titled “Can I extend Ensure with custom methods?”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.
Getting Help
Section titled “Getting Help”- GitHub Issues: github.com/nicola-pragmatic/Pragmatic.Design/issues
- API Reference: See api-reference.md for complete method signatures
- Best Practices: See best-practices.md for guard placement rules
- Common Mistakes: See common-mistakes.md for Wrong/Right/Why patterns