Table of Contents

Class FluentValidationResultExtensions

Namespace
FunctionalDdd
Assembly
FunctionalDdd.FluentValidation.dll

Provides extension methods to integrate FluentValidation with Railway Oriented Programming Result types. Enables seamless conversion of FluentValidation results to Result<T> for functional error handling.

public static class FluentValidationResultExtensions
Inheritance
FluentValidationResultExtensions
Inherited Members

Examples

Complete example with aggregate, validator, and factory method:

// Define the aggregate
public class User : Aggregate<UserId>
{
    public FirstName FirstName { get; }
    public LastName LastName { get; }
    public EmailAddress Email { get; }
    public string Password { get; }

    // Factory method with validation
    public static Result<User> TryCreate(
        FirstName firstName, 
        LastName lastName, 
        EmailAddress email, 
        string password)
    {
        var user = new User(firstName, lastName, email, password);
        return s_validator.ValidateToResult(user);
    }

    private User(FirstName firstName, LastName lastName, EmailAddress email, string password)
        : base(UserId.NewUnique())
    {
        FirstName = firstName;
        LastName = lastName;
        Email = email;
        Password = password;
    }

    // Inline validator for business rules
    private static readonly InlineValidator<User> s_validator = new()
    {
        v => v.RuleFor(x => x.FirstName).NotNull(),
        v => v.RuleFor(x => x.LastName).NotNull(),
        v => v.RuleFor(x => x.Email).NotNull(),
        v => v.RuleFor(x => x.Password)
            .Matches("(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9])(?=.{8,})")
            .WithMessage("Password must be at least 8 characters with uppercase, lowercase, digit and special character")
    };
}

// Usage in application code
var result = User.TryCreate(firstName, lastName, email, password);
// Returns: Success(user) or Failure(ValidationError with field errors)

Using with API request validation:

// Request DTO
public record CreateUserRequest(string FirstName, string LastName, string Email, string Password);

// FluentValidation validator
public class CreateUserRequestValidator : AbstractValidator<CreateUserRequest>
{
    public CreateUserRequestValidator()
    {
        RuleFor(x => x.FirstName)
            .NotEmpty().WithMessage("First name is required")
            .MaximumLength(50);

        RuleFor(x => x.LastName)
            .NotEmpty().WithMessage("Last name is required")
            .MaximumLength(50);

        RuleFor(x => x.Email)
            .NotEmpty()
            .EmailAddress().WithMessage("Invalid email format");

        RuleFor(x => x.Password)
            .MinimumLength(8)
            .Matches("[A-Z]").WithMessage("Password must contain uppercase")
            .Matches("[a-z]").WithMessage("Password must contain lowercase")
            .Matches("[0-9]").WithMessage("Password must contain digit");
    }
}

// API endpoint
app.MapPost("/users", (CreateUserRequest request, IUserService service) =>
{
    var validator = new CreateUserRequestValidator();
    return validator.ValidateToResult(request)
        .Bind(req => FirstName.TryCreate(req.FirstName)
            .Combine(LastName.TryCreate(req.LastName))
            .Combine(EmailAddress.TryCreate(req.Email))
            .Bind((first, last, email) => service.CreateUser(first, last, email, req.Password)))
        .ToHttpResult();
});

// Validation errors automatically formatted with field names

Remarks

This class bridges FluentValidation's imperative validation model with FunctionalDDD's Railway Oriented Programming approach. Key benefits:

  • Automatic conversion of validation errors to domain ValidationError type
  • Field-level error grouping for structured error responses
  • Integration with Result type for consistent error handling
  • Support for both sync and async validation scenarios
  • Null-safety with automatic parameter name capture

Common usage patterns:

  • Factory method validation in aggregates and entities
  • DTO/request validation in API endpoints
  • Complex business rule validation with FluentValidation's powerful API
  • Chaining validation with other Result operations using Bind

Methods

ToResult<T>(ValidationResult, T)

Converts a FluentValidation FluentValidation.Results.ValidationResult to a Result<TValue>.

public static Result<T> ToResult<T>(this ValidationResult validationResult, T value)

Parameters

validationResult ValidationResult

The FluentValidation result containing validation state and errors.

value T

The value that was validated.

Returns

Result<T>
  • Success containing the value if validation passed
  • Failure with validation errors if validation failed or value is null

Type Parameters

T

The type of the value being validated.

Examples

Manual validation and conversion:

var validator = new UserValidator();
var user = new User { Name = "", Email = "invalid" };
var validationResult = validator.Validate(user);
var result = validationResult.ToResult(user);

if (result.IsFailure)
{
    var error = (ValidationError)result.Error;
    foreach (var fieldError in error.FieldErrors)
    {
        Console.WriteLine($"{fieldError.FieldName}: {string.Join(", ", fieldError.Details)}");
    }
}
// Output:
// Name: Name is required
// Email: Email must be a valid email address

Remarks

This method groups validation errors by property name, making it easy to display field-level errors in UIs or API responses. Errors are converted to immutable FieldError objects.

The resulting ValidationError can be automatically converted to:

  • HTTP 400 Bad Request with validation problem details (via ToActionResult/ToHttpResult)
  • Structured error responses with field-level error messages

ValidateToResultAsync<T>(IValidator<T>, T, string, string?, CancellationToken)

Asynchronously validates the specified value using FluentValidation and converts the result to Result<TValue>.

public static Task<Result<T>> ValidateToResultAsync<T>(this IValidator<T> validator, T value, string paramName = "value", string? message = null, CancellationToken cancellationToken = default)

Parameters

validator IValidator<T>

The FluentValidation validator to use.

value T

The value to validate.

paramName string

The parameter name for error messages. Automatically captured from the caller expression.

message string

Optional custom error message when value is null.

cancellationToken CancellationToken

Cancellation token to observe.

Returns

Task<Result<T>>

A task representing the asynchronous validation operation, containing:

  • Success with the value if validation passed
  • Failure with validation errors if validation failed or value is null

Type Parameters

T

The type of the value being validated.

Examples

Using with async validation rules:

public class CreateUserRequestValidator : AbstractValidator<CreateUserRequest>
{
    private readonly IUserRepository _repository;

    public CreateUserRequestValidator(IUserRepository repository)
    {
        _repository = repository;

        RuleFor(x => x.Email)
            .NotEmpty()
            .EmailAddress()
            .MustAsync(async (email, ct) => 
                !await _repository.ExistsByEmailAsync(email, ct))
            .WithMessage("Email already in use");
    }
}

// API endpoint with async validation
app.MapPost("/users", async (
    CreateUserRequest request,
    CreateUserRequestValidator validator,
    IUserService service,
    CancellationToken ct) =>
    await validator.ValidateToResultAsync(request, cancellationToken: ct)
        .BindAsync(req => service.CreateUserAsync(req, ct), ct)
        .ToHttpResultAsync());

Async validation in application service:

public async Task<Result<Product>> CreateProductAsync(
    CreateProductRequest request,
    CancellationToken ct)
{
    return await _createProductValidator.ValidateToResultAsync(request, cancellationToken: ct)
        .BindAsync(req => ProductName.TryCreate(req.Name), ct)
        .BindAsync(name => Money.TryCreate(req.Price), ct)
        .BindAsync((name, price) => Product.CreateAsync(name, price, ct), ct)
        .TapAsync(async product => await _repository.AddAsync(product, ct), ct);
}

Handling validation errors in the caller:

var result = await validator.ValidateToResultAsync(request, cancellationToken: ct);

return result.Match(
    onSuccess: user => Ok(new UserDto(user)),
    onFailure: error => error is ValidationError validationError
        ? BadRequest(validationError.FieldErrors)
        : StatusCode(500, error.Detail)
);

Remarks

This async variant is essential when validation rules perform async operations such as:

  • Database uniqueness checks
  • External API validations
  • File system validations
  • Any I/O-bound validation logic

Like the synchronous variant, this method provides null-safety and automatic parameter name capture for informative error messages.

Respects cancellation tokens to enable responsive cancellation of long-running validations.

ValidateToResult<T>(IValidator<T>, T, string, string?)

Validates the specified value using FluentValidation and converts the result to Result<TValue>.

public static Result<T> ValidateToResult<T>(this IValidator<T> validator, T value, string paramName = "value", string? message = null)

Parameters

validator IValidator<T>

The FluentValidation validator to use.

value T

The value to validate.

paramName string

The parameter name for error messages. Automatically captured from the caller expression.

message string

Optional custom error message when value is null.

Returns

Result<T>
  • Success containing the value if validation passed
  • Failure with validation errors if validation failed or value is null

Type Parameters

T

The type of the value being validated.

Examples

Using in an aggregate factory method:

public class Order : Aggregate<OrderId>
{
    public CustomerId CustomerId { get; }
    public IReadOnlyList<OrderLine> Lines { get; }

    public static Result<Order> TryCreate(CustomerId customerId, List<OrderLine> lines)
    {
        var order = new Order(customerId, lines);
        return s_validator.ValidateToResult(order);
    }

    private static readonly InlineValidator<Order> s_validator = new()
    {
        v => v.RuleFor(x => x.CustomerId).NotNull(),
        v => v.RuleFor(x => x.Lines)
            .NotEmpty().WithMessage("Order must have at least one line")
            .Must(lines => lines.Count <= 100).WithMessage("Order cannot exceed 100 lines")
    };
}

Chaining validation with other operations:

public Result<User> CreateUser(CreateUserRequest request)
{
    return _requestValidator.ValidateToResult(request)
        .Bind(req => EmailAddress.TryCreate(req.Email)
            .Combine(FirstName.TryCreate(req.FirstName))
            .Bind((email, name) => User.TryCreate(email, name, req.Password)))
        .Tap(user => _repository.Add(user));
}

Null-safety demonstration:

var validator = new UserValidator();
User? nullUser = null;
var result = validator.ValidateToResult(nullUser);
// Returns: Failure with error "'nullUser' must not be empty."
// Validator.Validate() is never called

Remarks

This method provides null-safety by checking for null values before validation. If the value is null, it returns a validation failure without calling the validator.

The parameter name is automatically captured using [CallerArgumentExpression], making error messages more informative without manual string entry.

Common usage patterns:

  • Aggregate/Entity factory methods
  • Value object creation with complex validation rules
  • DTO validation before processing
  • Integration with Bind/Map for validation chains