Claude-skill-registry fluentvalidation-patterns
Master FluentValidation patterns for ABP Framework including async validators, repository checks, conditional rules, localized messages, and custom validators. Use when creating input DTO validators for AppServices.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/fluentvalidation-patterns" ~/.claude/skills/majiayu000-claude-skill-registry-fluentvalidation-patterns && rm -rf "$T"
manifest:
skills/data/fluentvalidation-patterns/SKILL.mdsource content
FluentValidation Patterns
FluentValidation patterns for ABP Framework input DTO validation.
When to Use
- Creating validators for Create/Update DTOs
- Implementing async validation with repository checks
- Building conditional validation rules
- Creating reusable custom validators
- Localizing validation error messages
ABP Integration
Basic Validator Structure
// Application/{Feature}/CreateUpdate{Entity}DtoValidator.cs public class CreateUpdate{Entity}DtoValidator : AbstractValidator<CreateUpdate{Entity}Dto> { public CreateUpdatePatientDtoValidator() { RuleFor(x => x.FirstName) .NotEmpty() .MaximumLength(100); RuleFor(x => x.LastName) .NotEmpty() .MaximumLength(100); RuleFor(x => x.Email) .NotEmpty() .EmailAddress() .MaximumLength(255); RuleFor(x => x.DateOfBirth) .NotEmpty() .LessThan(DateTime.Today) .WithMessage("Date of birth must be in the past"); } }
ABP Module Registration
// ApplicationModule.cs public override void PreConfigureServices(ServiceConfigurationContext context) { PreConfigure<AbpFluentValidationAutoValidationOptions>(options => { options.AutoValidateMethodInvocations = true; }); } public override void ConfigureServices(ServiceConfigurationContext context) { // Auto-register all validators in assembly context.Services.AddValidatorsFromAssembly(typeof({ProjectName}ApplicationModule).Assembly); }
Common Validation Rules
String Validation
RuleFor(x => x.Name) .NotEmpty() .WithMessage("Name is required") .MinimumLength(2) .MaximumLength(100) .Matches(@"^[a-zA-Z\s]+$") .WithMessage("Name can only contain letters and spaces"); RuleFor(x => x.Email) .NotEmpty() .EmailAddress() .Must(email => !email.EndsWith("@test.com")) .WithMessage("Test emails are not allowed"); RuleFor(x => x.Phone) .NotEmpty() .Matches(@"^\+?[1-9]\d{1,14}$") .WithMessage("Invalid phone number format");
Numeric Validation
RuleFor(x => x.Age) .InclusiveBetween(0, 150) .WithMessage("Age must be between 0 and 150"); RuleFor(x => x.Price) .GreaterThan(0) .LessThanOrEqualTo(1000000) .PrecisionScale(10, 2, false); RuleFor(x => x.Quantity) .GreaterThanOrEqualTo(1) .When(x => x.IsRequired);
Date Validation
RuleFor(x => x.DateOfBirth) .NotEmpty() .LessThan(DateTime.Today) .GreaterThan(DateTime.Today.AddYears(-150)); RuleFor(x => x.AppointmentDate) .NotEmpty() .GreaterThan(DateTime.Now) .WithMessage("Appointment must be in the future"); RuleFor(x => x.EndDate) .GreaterThan(x => x.StartDate) .When(x => x.EndDate.HasValue) .WithMessage("End date must be after start date");
Collection Validation
RuleFor(x => x.Tags) .NotEmpty() .Must(tags => tags.Count <= 10) .WithMessage("Maximum 10 tags allowed"); RuleForEach(x => x.Items) .ChildRules(item => { item.RuleFor(i => i.ProductId).NotEmpty(); item.RuleFor(i => i.Quantity).GreaterThan(0); }); RuleFor(x => x.Emails) .Must(emails => emails.Distinct().Count() == emails.Count) .WithMessage("Duplicate emails are not allowed");
Async Validation with Repository
Unique Email Check
public class CreateUpdate{Entity}DtoValidator : AbstractValidator<CreateUpdate{Entity}Dto> { private readonly IRepository<{Entity}, Guid> _repository; public CreateUpdate{Entity}DtoValidator(IRepository<{Entity}, Guid> repository) { _repository = repository; RuleFor(x => x.Email) .NotEmpty() .EmailAddress() .MustAsync(BeUniqueEmail) .WithMessage("Email already exists"); } private async Task<bool> BeUniqueEmail(string email, CancellationToken cancellationToken) { return !await _repository.AnyAsync(e => e.Email == email); } }
Unique Check with Edit Context
public class CreateUpdate{Entity}DtoValidator : AbstractValidator<CreateUpdate{Entity}Dto> { private readonly IRepository<{Entity}, Guid> _repository; private readonly IHttpContextAccessor _httpContextAccessor; public CreateUpdate{Entity}DtoValidator( IRepository<{Entity}, Guid> repository, IHttpContextAccessor httpContextAccessor) { _repository = repository; _httpContextAccessor = httpContextAccessor; RuleFor(x => x.Email) .NotEmpty() .EmailAddress() .MustAsync(BeUniqueEmailAsync) .WithMessage("Email already exists"); } private async Task<bool> BeUniqueEmailAsync(string email, CancellationToken cancellationToken) { // Get entity ID from route (for updates) var routeData = _httpContextAccessor.HttpContext?.GetRouteData(); var idString = routeData?.Values["id"]?.ToString(); if (Guid.TryParse(idString, out var entityId)) { // Exclude current entity from check (update scenario) return !await _repository.AnyAsync( e => e.Email == email && e.Id != entityId); } // Create scenario return !await _repository.AnyAsync(e => e.Email == email); } }
Foreign Key Exists Check
public class Create{Entity}DtoValidator : AbstractValidator<Create{Entity}Dto> { private readonly IRepository<{ParentEntity}, Guid> _parentRepository; private readonly IRepository<{RelatedEntity}, Guid> _relatedRepository; public Create{Entity}DtoValidator( IRepository<{ParentEntity}, Guid> parentRepository, IRepository<{RelatedEntity}, Guid> relatedRepository) { _parentRepository = parentRepository; _relatedRepository = relatedRepository; RuleFor(x => x.{ParentEntity}Id) .NotEmpty() .MustAsync(ParentExistsAsync) .WithMessage("{ParentEntity} not found"); RuleFor(x => x.{RelatedEntity}Id) .NotEmpty() .MustAsync(RelatedExistsAsync) .WithMessage("{RelatedEntity} not found"); } private async Task<bool> ParentExistsAsync(Guid id, CancellationToken ct) { return await _parentRepository.AnyAsync(e => e.Id == id); } private async Task<bool> RelatedExistsAsync(Guid id, CancellationToken ct) { return await _relatedRepository.AnyAsync(e => e.Id == id); } }
Conditional Validation
When/Unless
RuleFor(x => x.CompanyName) .NotEmpty() .When(x => x.CustomerType == CustomerType.Business) .WithMessage("Company name is required for business customers"); RuleFor(x => x.TaxId) .NotEmpty() .Matches(@"^\d{9}$") .When(x => x.CustomerType == CustomerType.Business); RuleFor(x => x.BirthDate) .NotEmpty() .Unless(x => x.CustomerType == CustomerType.Business);
Complex Conditional Logic
RuleFor(x => x.EndDate) .NotEmpty() .GreaterThan(x => x.StartDate) .When(x => x.IsRecurring && x.RecurrenceType != RecurrenceType.Indefinite); When(x => x.PaymentMethod == PaymentMethod.CreditCard, () => { RuleFor(x => x.CardNumber).NotEmpty().CreditCard(); RuleFor(x => x.ExpirationDate).NotEmpty().GreaterThan(DateTime.Today); RuleFor(x => x.CVV).NotEmpty().Length(3, 4); }); When(x => x.PaymentMethod == PaymentMethod.BankTransfer, () => { RuleFor(x => x.BankAccountNumber).NotEmpty(); RuleFor(x => x.RoutingNumber).NotEmpty(); });
Custom Validators
Reusable Property Validator
// Validators/PhoneNumberValidator.cs public class PhoneNumberValidator<T> : PropertyValidator<T, string> { public override string Name => "PhoneNumberValidator"; public override bool IsValid(ValidationContext<T> context, string value) { if (string.IsNullOrEmpty(value)) return true; // Let NotEmpty handle required check // E.164 format return Regex.IsMatch(value, @"^\+[1-9]\d{1,14}$"); } protected override string GetDefaultMessageTemplate(string errorCode) => "'{PropertyName}' must be a valid phone number in E.164 format"; } // Extension method for fluent usage public static class ValidatorExtensions { public static IRuleBuilderOptions<T, string> PhoneNumber<T>( this IRuleBuilder<T, string> ruleBuilder) { return ruleBuilder.SetValidator(new PhoneNumberValidator<T>()); } } // Usage RuleFor(x => x.Phone).PhoneNumber();
Async Custom Validator
public class UniqueEmailValidator<T> : AsyncPropertyValidator<T, string> { private readonly IRepository<{Entity}, Guid> _repository; public UniqueEmailValidator(IRepository<{Entity}, Guid> repository) { _repository = repository; } public override string Name => "UniqueEmailValidator"; public override async Task<bool> IsValidAsync( ValidationContext<T> context, string value, CancellationToken cancellation) { if (string.IsNullOrEmpty(value)) return true; return !await _repository.AnyAsync(e => e.Email == value); } protected override string GetDefaultMessageTemplate(string errorCode) => "'{PropertyName}' must be unique"; }
Localization
Using ABP Localization
public class CreateUpdate{Entity}DtoValidator : AbstractValidator<CreateUpdate{Entity}Dto> { private readonly IStringLocalizer<{ProjectName}Resource> _localizer; public CreateUpdate{Entity}DtoValidator( IStringLocalizer<{ProjectName}Resource> localizer) { _localizer = localizer; RuleFor(x => x.FirstName) .NotEmpty() .WithMessage(_localizer["Validation:FirstNameRequired"]) .MaximumLength(100) .WithMessage(_localizer["Validation:FirstNameMaxLength", 100]); RuleFor(x => x.Email) .NotEmpty() .WithMessage(_localizer["Validation:EmailRequired"]) .EmailAddress() .WithMessage(_localizer["Validation:EmailInvalid"]); } } // Localization JSON: Localization/{ProjectName}/en.json { "Validation:FirstNameRequired": "First name is required", "Validation:FirstNameMaxLength": "First name cannot exceed {0} characters", "Validation:EmailRequired": "Email is required", "Validation:EmailInvalid": "Please enter a valid email address" }
Validation in AppService
Manual Validation
public class {Entity}AppService : ApplicationService, I{Entity}AppService { private readonly IValidator<CreateUpdate{Entity}Dto> _validator; public async Task<{Entity}Dto> CreateAsync(CreateUpdate{Entity}Dto input) { // Manual validation (if auto-validation disabled) var validationResult = await _validator.ValidateAsync(input); if (!validationResult.IsValid) { throw new AbpValidationException( validationResult.Errors .Select(e => new ValidationResult(e.ErrorMessage, new[] { e.PropertyName })) .ToList() ); } // Proceed with creation } }
Validation with Custom Context
public async Task<{Entity}Dto> UpdateAsync(Guid id, CreateUpdate{Entity}Dto input) { var context = new ValidationContext<CreateUpdate{Entity}Dto>(input) { RootContextData = { ["EntityId"] = id } }; var validationResult = await _validator.ValidateAsync(context); // ... } // In validator private async Task<bool> BeUniqueEmailAsync( CreateUpdate{Entity}Dto dto, string email, ValidationContext<CreateUpdate{Entity}Dto> context, CancellationToken ct) { var entityId = context.RootContextData.TryGetValue("EntityId", out var id) ? (Guid?)id : null; if (entityId.HasValue) { return !await _repository.AnyAsync(p => p.Email == email && p.Id != entityId); } return !await _repository.AnyAsync(p => p.Email == email); }
Best Practices
- One validator per DTO - Keep validators focused
- Inject dependencies - Use constructor injection for repositories
- Use async for DB checks - Always use
for repository queriesMustAsync - Localize messages - Use ABP localization for user-facing messages
- Reuse validators - Create custom validators for common patterns
- Conditional rules - Use
/When
for context-dependent validationUnless - Order matters - Put cheap validations first, async last
- Handle null gracefully - Let
handle required checksNotEmpty
Quality Checklist
- Validator created for each input DTO
- All required fields have
ruleNotEmpty() - String fields have
matching entityMaximumLength() - Email fields use
validatorEmailAddress() - Foreign keys validated with
existence checkMustAsync - Unique constraints validated with repository check
- Error messages are localized
- Validators registered in module
Shared Knowledge
For foundational patterns, see the shared knowledge base:
| Topic | File | Description |
|---|---|---|
| Naming conventions | knowledge/conventions/naming.md | Validator naming patterns |
| Validation example | knowledge/examples/validation-chain.md | Complete validation examples |
| Folder structure | knowledge/conventions/folder-structure.md | Validator file locations |
Integration Points
This skill is used by:
- abp-developer: DTO validator implementation
- abp-code-reviewer: Validation pattern review
- /generate:entity: Validator scaffolding