From src/Application/Permissions/Commands/CreatePermission/CreatePermissionCommandValidator.cs:4:
CreatePermissionCommandValidator.cs
public class CreatePermissionCommandValidator : AbstractValidator<CreatePermissionCommand>{ public CreatePermissionCommandValidator() { RuleFor(x => x.Name) .NotEmpty().WithMessage("El nombre del permiso es requerido") .MaximumLength(100).WithMessage("El nombre no puede exceder 100 caracteres") .Matches("^[a-z0-9._-]+$").WithMessage("Use formato: modulo.accion (ej: users.create)"); RuleFor(x => x.Module) .NotEmpty().WithMessage("El módulo es requerido") .MaximumLength(50).WithMessage("El módulo no puede exceder 50 caracteres"); RuleFor(x => x.Description) .MaximumLength(500).WithMessage("La descripción no puede exceder 500 caracteres") .When(x => !string.IsNullOrEmpty(x.Description)); }}
public class UpdateUserCommandValidator : AbstractValidator<UpdateUserCommand>{ public UpdateUserCommandValidator() { // Password confirmation must match RuleFor(x => x.PasswordConfirmation) .Equal(x => x.Password) .When(x => !string.IsNullOrEmpty(x.Password)) .WithMessage("Password confirmation must match password"); // End date must be after start date RuleFor(x => x.EndDate) .GreaterThan(x => x.StartDate) .When(x => x.EndDate.HasValue) .WithMessage("End date must be after start date"); }}
From src/Application/Users/Commands/Register/RegisterCommandValidator.cs:
RegisterCommandValidator.cs
public class RegisterCommandValidator : AbstractValidator<RegisterCommand>{ public RegisterCommandValidator() { RuleFor(x => x.Email) .NotEmpty().WithMessage("Email is required") .EmailAddress().WithMessage("Invalid email format") .MaximumLength(256).WithMessage("Email cannot exceed 256 characters"); RuleFor(x => x.Password) .NotEmpty().WithMessage("Password is required") .MinimumLength(8).WithMessage("Password must be at least 8 characters") .Matches(@"[A-Z]").WithMessage("Password must contain at least one uppercase letter") .Matches(@"[a-z]").WithMessage("Password must contain at least one lowercase letter") .Matches(@"[0-9]").WithMessage("Password must contain at least one number") .Matches(@"[^a-zA-Z0-9]").WithMessage("Password must contain at least one special character"); RuleFor(x => x.Username) .NotEmpty().WithMessage("Username is required") .MinimumLength(3).WithMessage("Username must be at least 3 characters") .MaximumLength(50).WithMessage("Username cannot exceed 50 characters") .Matches(@"^[a-zA-Z0-9_-]+$").WithMessage("Username can only contain letters, numbers, underscores, and hyphens"); RuleFor(x => x.PhoneNumber) .Matches(@"^\+?[1-9]\d{1,14}$") .When(x => !string.IsNullOrEmpty(x.PhoneNumber)) .WithMessage("Phone number must be in valid E.164 format"); }}
When validation fails, the API returns a 400 Bad Request with:
{ "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "errors": { "Email": [ "Email is required", "Invalid email format" ], "Password": [ "Password must be at least 8 characters", "Password must contain at least one uppercase letter" ] }}
public class CreatePermissionCommandValidatorTests{ private readonly CreatePermissionCommandValidator _validator; public CreatePermissionCommandValidatorTests() { _validator = new CreatePermissionCommandValidator(); } [Test] public void Should_Have_Error_When_Name_Is_Empty() { // Arrange var command = new CreatePermissionCommand { Name = "" }; // Act var result = _validator.TestValidate(command); // Assert result.ShouldHaveValidationErrorFor(x => x.Name) .WithErrorMessage("El nombre del permiso es requerido"); } [Test] public void Should_Have_Error_When_Name_Format_Is_Invalid() { // Arrange var command = new CreatePermissionCommand { Name = "Invalid Name!" }; // Act var result = _validator.TestValidate(command); // Assert result.ShouldHaveValidationErrorFor(x => x.Name) .WithErrorMessage("Use formato: modulo.accion (ej: users.create)"); } [Test] public void Should_Not_Have_Error_When_Command_Is_Valid() { // Arrange var command = new CreatePermissionCommand { Name = "users.create", Module = "Users", Description = "Create new users" }; // Act var result = _validator.TestValidate(command); // Assert result.ShouldNotHaveAnyValidationErrors(); }}
public static class CommonValidators{ public static IRuleBuilderOptions<T, string> MustBeValidPermissionName<T>( this IRuleBuilder<T, string> ruleBuilder) { return ruleBuilder .NotEmpty().WithMessage("Permission name is required") .Matches("^[a-z0-9._-]+$").WithMessage("Use format: module.action (e.g., users.create)") .MaximumLength(100).WithMessage("Permission name cannot exceed 100 characters"); }}// UsageRuleFor(x => x.Name).MustBeValidPermissionName();
public class UserValidator : AbstractValidator<User>{ public UserValidator() { // Stop validating Email after first failure RuleFor(x => x.Email) .Cascade(CascadeMode.Stop) .NotEmpty() .EmailAddress() .MustAsync(BeUniqueEmail); // Only runs if previous rules pass }}