Skip to main content

RA.Utilities.Application.Validation

NuGet version NuGet Downloads Codecov Publish NuGet GitHub license

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 BadRequestException with structured ValidationError entries.
  • Share rules with the domain — the MustBeValid* rules construct the value objects from RA.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 array
  • ValidationUtilities.CreateValidationErrorResult — maps ValidationFailure[] into a BadRequestException carrying PropertyName, ErrorMessage, AttemptedValue, and ErrorCode
  • Cooperative cancellation — the CancellationToken is forwarded to every validator
  • MustMatchesCurrencyFormat() — declarative ISO 4217 shape rule (3 uppercase letters) with a source-generated regex
  • MustBeValidCurrency() / MustBeValidEmail() / MustBeValidSsn() / MustBeValidIban() — rules that validate string? 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
Version 10.2.0

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​

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​

AbstractionRoleReference
ValidationUtilitiesOrchestrates validation and converts failures to an exceptionValidationUtilities
MustMatchesCurrencyFormat()ISO 4217 shape rule (3 uppercase letters)CurrencyValidatorExtensions
MustBeValidCurrency()Currency rule backed by the Currency value objectCurrencyValidatorExtensions
MustBeValidEmail()Email rule backed by the Email value objectEmailValidatorExtensions
MustBeValidSsn()SSN rule backed by the SSN value objectSsnValidatorExtensions
MustBeValidIban()IBAN rule backed by the IBAN value objectIbanValidatorExtensions
BadRequestException / ValidationErrorThe standardized 400 payload produced on failureRA.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 rulesValue-object rules
ExampleMustMatchesCurrencyFormat()MustBeValidCurrency(), MustBeValidEmail(), MustBeValidSsn(), MustBeValidIban()
ChecksShape onlyThe full rules of the value object
null / emptynull passes the format rule; "" failsFails — the value object requires a value
Failure messageRule-specific message (e.g. "Currency must be 3 uppercase letters (A-Z).")The generic BadRequestException message ("The request is invalid.")
CustomizationWithMessage(...)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
}
}
FluentValidation DI package

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>> from AddValidatorsFromAssemblyContaining<T>().
  • Error messages are customized per rule with FluentValidation's WithMessage(...).
  • Language / localization is FluentValidation's concern; configure ValidatorOptions.Global.LanguageManager in your application if needed.

🔗 Dependencies​

PackageVersionWhy
FluentValidation12.1.1Rule builders, validators, ValidationFailure
RA.Utilities.Core.Exceptions10.1.0+BadRequestException, ValidationError
RA.Utilities.Core.ValueObjects10.2.0Currency, Email, SSN, IBAN used by the MustBeValid* rules

✨ Available Types​


🙌 Contributing & Support​

Found a bug or have a suggestion?​

Contributing​

  1. Fork the RA.Utilities repository
  2. Create a branch from main with a descriptive name (e.g., feature/add-phone-validator)
  3. Make your changes — new rules need XML doc comments, tests covering valid/invalid/missing input, and a migration note if behavior changes
  4. Submit a pull request to the main branch with a clear description
ResourceURL
NuGetRA.Utilities.Application.Validation
SourceGitHub — RedonAlla/RA.Utilities
LicenseMIT
Build StatusGitHub Actions