Troubleshooting & FAQ
Common Issues
I see duplicate validation failures — the same error appears once per validator
Cause: you are on v10.0.0, which shared one ValidationContext across all validators in ValidateAsync. Every validator's result contained the full shared failure list.
Fix: upgrade to 10.0.1, which creates a fresh context per validator. Remove any de-duplication workarounds.
CS7036 on ValidateAsync after upgrading to 10.1.0
Cause: 10.1.0 added a required CancellationToken parameter to ValidationUtilities.ValidateAsync — the old two-argument calls no longer compile.
Fix: pass CancellationToken.None, or forward the token from your request pipeline:
ValidationFailure[] failures = await ValidationUtilities.ValidateAsync(request, validators, cancellationToken);
See the migration guide for the full v10.0.1 → v10.1.0 upgrade notes.
My optional currency field fails validation when it is null
Cause: v10.0.0 ran the format rule on null values (FluentValidation 12 executes Must on null).
Fix: upgrade to 10.0.1 (null passes the format rule), or make the requirement explicit:
// Currency is required
RuleFor(x => x.Currency).NotNull().MustMatchesCurrencyFormat();
// Currency is optional — null and valid codes pass, empty still fails
RuleFor(x => x.Currency).MustMatchesCurrencyFormat();
An empty string is rejected by MustMatchesCurrencyFormat — I want empty to be allowed
Cause: the format rule intentionally fails empty strings; an empty string is not a currency code.
Fix: allow empty values explicitly with a condition:
RuleFor(x => x.Currency)
.MustMatchesCurrencyFormat()
.When(x => !string.IsNullOrEmpty(x.Currency));
Can I customize the error message?
Answer: yes — chain FluentValidation's WithMessage (and {PropertyName} is substituted automatically):
RuleFor(x => x.Currency)
.MustMatchesCurrencyFormat()
.WithMessage("'{PropertyName}' must be a valid ISO 4217 currency code.");
Can I run validators sequentially instead of in parallel?
Answer: ValidateAsync always runs validators in parallel. If ordering or side effects require sequential execution, enumerate the validators yourself:
var failures = new List<ValidationFailure>();
foreach (IValidator<MyRequest> validator in validators)
failures.AddRange((await validator.ValidateAsync(request)).Errors);
How do I validate an IBAN?
Answer: chain MustBeValidIban() on the string property. It constructs the IBAN value object, so structure (13–34 characters) and the ISO 7064 mod-97 checksum are enforced:
using RA.Utilities.Application.Validation.Extensions;
RuleFor(x => x.Iban).MustBeValidIban();
The rule normalizes casing and grouping separators ("gb82 west 1234 5698 7654 32" passes) and fails on null, empty, and whitespace — it doubles as a required check.
Why does an IBAN that passes my country-length check still fail?
Cause: MustBeValidIban() uses the IBAN value object, which validates the 13–34 character structural range and the mod-97 checksum. It does not consult a per-country length table or the SWIFT registry, and it does not prove the account exists.
Fix: validate country-specific rules separately and report BaseErrorCode.IbanLength. See IbanValidatorExtensions.
Why is the failure message "The request is invalid."?
Cause: every MustBeValid* rule catches the BadRequestException thrown by the value object and uses ex.Message. The value object stores the specific detail (missing, wrong length, bad format, wrong check digits) in ex.Errors[0].ErrorMessage, while ex.Message is the generic BaseResponseMessages.BadRequest.
Fix: chain WithMessage for the client-facing text:
RuleFor(x => x.Iban)
.MustBeValidIban()
.WithMessage("'{PropertyName}' must be a valid IBAN.");
If you need the specific value-object reason, read it from the caught exception in a custom Custom rule instead of using the built-in helper.
Can I make an IBAN optional?
Answer: yes — skip the rule when the value is absent:
RuleFor(x => x.Iban)
.MustBeValidIban()
.When(x => !string.IsNullOrWhiteSpace(x.Iban));
This differs from MustMatchesCurrencyFormat(), where null passes by default and "" still fails. The MustBeValid* rules treat every missing form as invalid.
Does MustBeValidIban work with ValidateAsync?
Answer: yes. The rule is a FluentValidation Custom rule, so it runs for both Validate and ValidateAsync. IBAN validation itself is synchronous; the rule simply participates in the async pipeline.
Known Limitations
MustMatchesCurrencyFormatchecks format only — it does not verify that a code exists in the official ISO 4217 list. Validate membership against a curated list for strict requirements.- Cancellation is cooperative —
ValidateAsyncrequires aCancellationTokenand forwards it to every validator; validators with async rules honour it, while purely synchronous rules run to completion. - No DI registration helper — the package deliberately registers nothing; supply
IEnumerable<IValidator<T>>from your own container. - Value-object rules report generic messages —
MustBeValidCurrency(),MustBeValidEmail(),MustBeValidSsn(), andMustBeValidIban()surfaceBadRequestException.Message("The request is invalid."); useWithMessage(...)for specific text. - IBAN validation is structural —
MustBeValidIban()checks the 13–34 character format and ISO 7064 mod-97 checksum, not per-country lengths, registry membership, or account existence. MustMatchesCurrencyFormat()is shape-only — it acceptsU5Dand123; useMustBeValidCurrency()when only real ISO 4217 codes should pass.
Performance Tips
- Validators run in parallel — group independent rules across validators and let
Task.WhenAlldo the work. - Source-generated regex — the currency rule's
[GeneratedRegex]compiles the pattern into your assembly; no runtime regex compilation cost. - Validate once — reuse the same
ValidationFailure[]result for both logging and exception creation rather than callingValidateAsynctwice. - Value-object rules allocate one value object per property — the cost is small and buys domain-identical validation; for extremely hot paths, a compiled regex check may be cheaper but duplicates the domain rule.