RA.Utilities.Application.Validation
RA.Utilities.Application.Validation provides reusable validation utilities built on FluentValidation for validating incoming requests in web APIs.
Use it to run every validator for a request in parallel, aggregate the failures, and convert them into a standardized BadRequestException — which RA.Utilities.Api turns into a consistent HTTP 400 response — without writing the orchestration code yourself.
🎯 Purpose
Validation usually needs three things that FluentValidation leaves to you: running all validators for a request, merging their results safely, and producing one error payload. This package supplies all three, plus rules that reuse the domain's value objects:
- Centralize orchestration — one call runs every registered validator for a request.
- Standardize error payloads — failures become a
BadRequestExceptionwith structuredValidationErrorentries. - Share rules with the domain — the
MustBeValid*rules construct the value objects fromRA.Utilities.Core.ValueObjects, so a request is accepted exactly when the domain would accept it. - Stay fast — validators run concurrently, single/empty validator lists take fast paths, and the currency format rule uses a source-generated regex.
✨ Key Features
ValidationUtilities.ValidateAsync— runs all validators for a request concurrently and flattens the failures into one arrayValidationUtilities.CreateValidationErrorResult— mapsValidationFailure[]into aBadRequestExceptioncarryingPropertyName,ErrorMessage,AttemptedValue, andErrorCode- Cooperative cancellation — the
CancellationTokenis forwarded to every validator MustMatchesCurrencyFormat()— declarative ISO 4217 shape rule (3 uppercase letters) with a source-generated regexMustBeValidCurrency()/MustBeValidEmail()/MustBeValidSsn()/MustBeValidIban()— rules that validatestring?properties through the matching value object- IBAN validation — structure (13–34 characters) and ISO 7064 mod-97 check digits, with normalization of casing and grouping separators
- Zero configuration — static utilities; validators come from your own DI container
This release is additive. It adds MustBeValidIban() and updates the RA.Utilities.Core.ValueObjects dependency to 10.2.0, which introduces the IBAN value object. No existing rule, signature, or behavior changed. See the Migration Guides.
🛠️ Getting Started
Prerequisites
- .NET 10.0 or later
FluentValidation12.1.1 or later — installed as a dependencyRA.Utilities.Core.ExceptionsandRA.Utilities.Core.ValueObjects— installed automatically as dependencies
Installation
.NET CLI
dotnet add package RA.Utilities.Application.Validation
Package Manager Console
Install-Package RA.Utilities.Application.Validation
PackageReference (csproj)
<PackageReference Include="RA.Utilities.Application.Validation" Version="10.2.0" />
Hello World
Define a FluentValidation validator, run every validator for the request, and convert failures into the standardized exception:
using FluentValidation;
using FluentValidation.Results;
using RA.Utilities.Application.Validation.Extensions;
using RA.Utilities.Application.Validation.Utilities;
public sealed record CreatePaymentRequest(string Currency, string Iban, decimal Amount);
public sealed class CreatePaymentValidator : AbstractValidator<CreatePaymentRequest>
{
public CreatePaymentValidator()
{
RuleFor(x => x.Currency).MustBeValidCurrency();
RuleFor(x => x.Iban).MustBeValidIban();
RuleFor(x => x.Amount).GreaterThan(0);
}
}
Then run every validator for the request and convert failures into the standardized exception:
var request = new CreatePaymentRequest("EUR", "GB82WEST12345698765432", 100m);
var validators = new IValidator<CreatePaymentRequest>[] { new CreatePaymentValidator() };
ValidationFailure[] failures = await ValidationUtilities.ValidateAsync(
request,
validators,
CancellationToken.None);
if (failures.Length > 0)
{
throw ValidationUtilities.CreateValidationErrorResult(failures);
}
No DI registration or setup is required. The validators typically come from your container as IEnumerable<IValidator<TRequest>> — see Register validators with DI.
🧠 Core Concepts
Types
| Abstraction | Role | Reference |
|---|---|---|
ValidationUtilities | Orchestrates validation and converts failures to an exception | ValidationUtilities |
MustMatchesCurrencyFormat() | ISO 4217 shape rule (3 uppercase letters) | CurrencyValidatorExtensions |
MustBeValidCurrency() | Currency rule backed by the Currency value object | CurrencyValidatorExtensions |
MustBeValidEmail() | Email rule backed by the Email value object | EmailValidatorExtensions |
MustBeValidSsn() | SSN rule backed by the SSN value object | SsnValidatorExtensions |
MustBeValidIban() | IBAN rule backed by the IBAN value object | IbanValidatorExtensions |
BadRequestException / ValidationError | The standardized 400 payload produced on failure | RA.Utilities.Core.Exceptions |
How they work together
Request ──► ValidateAsync(request, validators, ct) ──► ValidationFailure[] (empty when valid)
│
└─► CreateValidationErrorResult ──► BadRequestException ──► HTTP 400
ValidateAsync gives each validator its own ValidationContext, so validators can run in parallel without sharing mutable state. FluentValidation's ValidationResult wraps its context's failure list, so a shared context would duplicate every failure into every result.
The method has two fast paths: an empty validator list returns an empty array immediately, and a single validator is awaited directly instead of going through Task.WhenAll.
Format rules vs value-object rules
| Format rules | Value-object rules | |
|---|---|---|
| Example | MustMatchesCurrencyFormat() | MustBeValidCurrency(), MustBeValidEmail(), MustBeValidSsn(), MustBeValidIban() |
| Checks | Shape only | The full rules of the value object |
null / empty | null passes the format rule; "" fails | Fails — the value object requires a value |
| Failure message | Rule-specific message (e.g. "Currency must be 3 uppercase letters (A-Z).") | The generic BadRequestException message ("The request is invalid.") |
| Customization | WithMessage(...) | WithMessage(...) |
Because value-object rules fail on null, they double as required checks. Format rules leave requiredness to NotNull() / NotEmpty().
Where the rules apply
The rules are declared as C# 14 extension members on IRuleBuilder<T, string> and constrain T to a reference type. They chain onto RuleFor(x => x.Property) for any string/string? property, including nested validators — failures are reported with the full property path.
Terminology
- Validator: a FluentValidation
IValidator<T>implementation holding the rules for one request type. - Validation failure: FluentValidation's
ValidationFailure— property name, message, attempted value, error code. - Format rule: a rule that checks the shape of a value (e.g. 3 uppercase letters), not its presence or domain validity.
- Value-object rule: a rule that constructs a value object to validate the input, so request and domain rules cannot drift apart.
📖 How-To Guides
Validate a request and return a 400 payload
ValidationFailure[] failures = await ValidationUtilities.ValidateAsync(
request,
validators,
cancellationToken);
if (failures.Length > 0)
{
throw ValidationUtilities.CreateValidationErrorResult(failures);
}
Throw inside a global exception handler pipeline, or use the ValidationBehavior from RA.Utilities.Feature, which performs these steps for every handler. See ValidationUtilities for the full API.
Register validators with DI
The utilities are static and do not scan assemblies. Register your validators with FluentValidation and inject them as a collection:
using FluentValidation;
using RA.Utilities.Application.Validation.Utilities;
builder.Services.AddValidatorsFromAssemblyContaining<CreatePaymentValidator>();
// In a handler or endpoint
public sealed class CreatePaymentHandler(IEnumerable<IValidator<CreatePaymentRequest>> validators)
{
public async Task HandleAsync(CreatePaymentRequest request, CancellationToken cancellationToken)
{
ValidationFailure[] failures = await ValidationUtilities.ValidateAsync(
request, validators, cancellationToken);
if (failures.Length > 0)
{
throw ValidationUtilities.CreateValidationErrorResult(failures);
}
// ... handle the request
}
}
AddValidatorsFromAssemblyContaining<T>() comes from the FluentValidation.DependencyInjectionExtensions package, not from this package. Add it alongside FluentValidation if you want assembly scanning, or register each IValidator<T> manually.
Add a currency format rule
RuleFor(x => x.Currency).MustMatchesCurrencyFormat();
// Required currency: reject null before checking the shape
RuleFor(x => x.Currency).NotNull().MustMatchesCurrencyFormat();
null passes the format rule; an empty string does not. See CurrencyValidatorExtensions for the full valid/invalid table.
Validate through value objects
public sealed record RegisterUserRequest(string Email, string Currency, string Ssn, string Iban);
public sealed class RegisterUserValidator : AbstractValidator<RegisterUserRequest>
{
public RegisterUserValidator()
{
RuleFor(x => x.Email).MustBeValidEmail();
RuleFor(x => x.Currency).MustBeValidCurrency();
RuleFor(x => x.Ssn).MustBeValidSsn();
RuleFor(x => x.Iban).MustBeValidIban();
}
}
These rules fail on null, empty, and whitespace input, and they normalize the value (case, surrounding whitespace, and — for IBAN — grouping separators) exactly as the value object does. See IbanValidatorExtensions for IBAN-specific behavior.
Provide a property-specific failure message
Value-object rules report the generic BadRequestException message because the value object's specific detail lives in the exception's Errors array, not its Message. Chain WithMessage when the client should see a targeted message:
RuleFor(x => x.Iban)
.MustBeValidIban()
.WithMessage("'{PropertyName}' must be a valid IBAN.");
Validate a nested object
The rules report the full property path, so failures on nested models identify the exact member:
public sealed class RemittanceValidator : AbstractValidator<RemittanceRequest>
{
public RemittanceValidator()
{
RuleFor(x => x.Beneficiary).SetValidator(new BeneficiaryValidator());
}
}
// A failure on Beneficiary.Iban reports PropertyName = "Beneficiary.Iban"
Run validation inside a mediator pipeline
RA.Utilities.Feature's ValidationBehavior calls ValidateAsync and CreateValidationErrorResult for every handler, so the same rules and payloads apply without repeating the orchestration:
services.AddFeature<CreatePayment, Result<PaymentId>, CreatePaymentHandler>()
.AddValidator<CreatePaymentValidator>();
See the RA.Utilities.Feature documentation for registration details.
⚙️ Configuration
This package has no configuration options and no options class. It registers nothing in DI.
- Validators are supplied by the caller, normally as
IEnumerable<IValidator<TRequest>>fromAddValidatorsFromAssemblyContaining<T>(). - Error messages are customized per rule with FluentValidation's
WithMessage(...). - Language / localization is FluentValidation's concern; configure
ValidatorOptions.Global.LanguageManagerin your application if needed.
🔗 Dependencies
| Package | Version | Why |
|---|---|---|
FluentValidation | 12.1.1 | Rule builders, validators, ValidationFailure |
RA.Utilities.Core.Exceptions | 10.1.0+ | BadRequestException, ValidationError |
RA.Utilities.Core.ValueObjects | 10.2.0 | Currency, Email, SSN, IBAN used by the MustBeValid* rules |
✨ Available Types
CurrencyValidatorExtensions
CurrencyValidatorExtensions adds two reusable FluentValidation rules for currency codes:
EmailValidatorExtensions
EmailValidatorExtensions adds a reusable FluentValidation rule that validates a string property through the Email value object of RA.Utilities.Core.ValueObjects — so request validation and the domain share one set of email rules.
SsnValidatorExtensions
SsnValidatorExtensions adds a reusable FluentValidation rule that validates a string property through the SSN value object of RA.Utilities.Core.ValueObjects — so request validation and the domain share one set of SSN rules.
IbanValidatorExtensions
IbanValidatorExtensions adds a reusable FluentValidation rule that validates a string property through the IBAN value object of RA.Utilities.Core.ValueObjects — so request validation and the domain share one set of IBAN rules.
ValidationUtilities
ValidationUtilities is a static helper that streamlines request validation with FluentValidation.
Migration Guides
v10.1.0 → v10.2.0
Troubleshooting & FAQ
Common Issues
🙌 Contributing & Support
Found a bug or have a suggestion?
- GitHub Issues: Report a bug or request a feature
- Discussions: Ask questions or start a discussion
Contributing
- Fork the RA.Utilities repository
- Create a branch from
mainwith a descriptive name (e.g.,feature/add-phone-validator) - Make your changes — new rules need XML doc comments, tests covering valid/invalid/missing input, and a migration note if behavior changes
- Submit a pull request to the
mainbranch with a clear description
Links
| Resource | URL |
|---|---|
| NuGet | RA.Utilities.Application.Validation |
| Source | GitHub — RedonAlla/RA.Utilities |
| License | MIT |
| Build Status | GitHub Actions |