Skip to main content

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​

  • MustMatchesCurrencyFormat checks 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 — ValidateAsync requires a CancellationToken and 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(), and MustBeValidIban() surface BadRequestException.Message ("The request is invalid."); use WithMessage(...) 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 accepts U5D and 123; use MustBeValidCurrency() when only real ISO 4217 codes should pass.

Performance Tips​

  • Validators run in parallel — group independent rules across validators and let Task.WhenAll do 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 calling ValidateAsync twice.
  • 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.