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
validationResultValidationResultThe FluentValidation result containing validation state and errors.
valueTThe 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
TThe 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
validatorIValidator<T>The FluentValidation validator to use.
valueTThe value to validate.
paramNamestringThe parameter name for error messages. Automatically captured from the caller expression.
messagestringOptional custom error message when value is null.
cancellationTokenCancellationTokenCancellation 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
TThe 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
validatorIValidator<T>The FluentValidation validator to use.
valueTThe value to validate.
paramNamestringThe parameter name for error messages. Automatically captured from the caller expression.
messagestringOptional 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
TThe 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