FluentValidation Rules Generator
Overview
FluentValidation provides a fluent interface for building strongly-typed validation rules:
- Declarative rules - Readable, maintainable validation logic
- Separation of concerns - Validation separate from domain
- Integration with MediatR - Automatic validation via pipeline behavior
- Custom validators - Reusable validation components
Quick Reference
| Validator Type | Purpose | Example |
|---|---|---|
| Built-in | Common validations | NotEmpty(), MaximumLength() |
| Custom | Reusable rules | Must(BeValidEmail) |
| Async | Database checks | MustAsync(BeUniqueEmail) |
| Child | Nested objects | SetValidator(new AddressValidator()) |
| Collection | List items | RuleForEach(x => x.Items) |
Validator Structure
/Application/{Feature}/
├── Create{Entity}/
│ ├── Create{Entity}Command.cs
│ └── Create{Entity}CommandValidator.cs # Or inline in Command.cs
├── Update{Entity}/
│ └── Update{Entity}Command.cs # Validator inline
└── Validators/
├── EmailValidator.cs # Reusable validators
└── PhoneNumberValidator.cs
Template: Basic Command Validator
// src/{name}.application/{Feature}/Create{Entity}/Create{Entity}CommandValidator.cs
using FluentValidation;
namespace {name}.application.{feature}.Create{Entity};
public sealed class Create{Entity}CommandValidator : AbstractValidator<Create{Entity}Command>
{
public Create{Entity}CommandValidator()
{
// ═══════════════════════════════════════════════════════════════
// STRING VALIDATIONS
// ═══════════════════════════════════════════════════════════════
RuleFor(x => x.Name)
.NotEmpty()
.WithMessage("{Entity} name is required")
.MaximumLength(100)
.WithMessage("{Entity} name must not exceed 100 characters")
.MinimumLength(2)
.WithMessage("{Entity} name must be at least 2 characters");
RuleFor(x => x.Description)
.MaximumLength(500)
.WithMessage("Description must not exceed 500 characters")
.When(x => !string.IsNullOrEmpty(x.Description));
// ═══════════════════════════════════════════════════════════════
// GUID VALIDATIONS
// ═══════════════════════════════════════════════════════════════
RuleFor(x => x.OrganizationId)
.NotEmpty()
.WithMessage("Organization ID is required")
.NotEqual(Guid.Empty)
.WithMessage("Organization ID cannot be empty GUID");
// ═══════════════════════════════════════════════════════════════
// OPTIONAL FOREIGN KEY
// ═══════════════════════════════════════════════════════════════
RuleFor(x => x.ParentId)
.NotEqual(Guid.Empty)
.WithMessage("Parent ID cannot be empty GUID")
.When(x => x.ParentId.HasValue);
}
}
Template: Inline Validator (Preferred Pattern)
// src/{name}.application/{Feature}/Create{Entity}/Create{Entity}Command.cs
using FluentValidation;
using {name}.application.abstractions.messaging;
using {name}.domain.abstractions;
namespace {name}.application.{feature}.Create{Entity};
// ═══════════════════════════════════════════════════════════════
// COMMAND
// ═══════════════════════════════════════════════════════════════
public sealed record Create{Entity}Command(
string Name,
string? Description,
Guid OrganizationId,
string Email,
decimal Amount,
List<CreateItemRequest> Items) : ICommand<Guid>;
public sealed class CreateItemRequest
{
public required string Name { get; init; }
public int Quantity { get; init; }
}
// ═══════════════════════════════════════════════════════════════
// VALIDATOR (internal, same file)
// ═══════════════════════════════════════════════════════════════
internal sealed class Create{Entity}CommandValidator : AbstractValidator<Create{Entity}Command>
{
public Create{Entity}CommandValidator()
{
RuleFor(x => x.Name)
.NotEmpty()
.MaximumLength(100);
RuleFor(x => x.Email)
.NotEmpty()
.EmailAddress()
.WithMessage("A valid email address is required");
RuleFor(x => x.Amount)
.GreaterThan(0)
.WithMessage("Amount must be positive")
.LessThanOrEqualTo(1_000_000)
.WithMessage("Amount cannot exceed 1,000,000");
RuleFor(x => x.Items)
.NotEmpty()
.WithMessage("At least one item is required")
.Must(items => items.Count <= 100)
.WithMessage("Cannot have more than 100 items");
RuleForEach(x => x.Items)
.ChildRules(item =>
{
item.RuleFor(i => i.Name)
.NotEmpty()
.MaximumLength(200);
item.RuleFor(i => i.Quantity)
.GreaterThan(0)
.LessThanOrEqualTo(10000);
});
}
}
// ═══════════════════════════════════════════════════════════════
// HANDLER
// ═══════════════════════════════════════════════════════════════
internal sealed class Create{Entity}CommandHandler
: ICommandHandler<Create{Entity}Command, Guid>
{
// ... implementation
}
Template: Async Validator with Database Check
// src/{name}.application/{Feature}/Create{Entity}/Create{Entity}CommandValidator.cs
using FluentValidation;
using {name}.domain.{aggregate};
namespace {name}.application.{feature}.Create{Entity};
internal sealed class Create{Entity}CommandValidator : AbstractValidator<Create{Entity}Command>
{
private readonly I{Entity}Repository _{entity}Repository;
private readonly IOrganizationRepository _organizationRepository;
public Create{Entity}CommandValidator(
I{Entity}Repository {entity}Repository,
IOrganizationRepository organizationRepository)
{
_{entity}Repository = {entity}Repository;
_organizationRepository = organizationRepository;
// Sync validations first (fast)
RuleFor(x => x.Name)
.NotEmpty()
.MaximumLength(100);
RuleFor(x => x.OrganizationId)
.NotEmpty();
// Async validations (database calls)
RuleFor(x => x.Name)
.MustAsync(BeUniqueName)
.WithMessage("A {entity} with this name already exists");
RuleFor(x => x.OrganizationId)
.MustAsync(OrganizationExists)
.WithMessage("Organization does not exist");
}
private async Task<bool> BeUniqueName(string name, CancellationToken ct)
{
var existing = await _{entity}Repository.GetByNameAsync(name, ct);
return existing is null;
}
private async Task<bool> OrganizationExists(Guid organizationId, CancellationToken ct)
{
return await _organizationRepository.ExistsAsync(organizationId, ct);
}
}
Note: Prefer doing existence checks in the Handler rather than Validator for better separation of concerns and testability. Use async validation sparingly.
Template: Reusable Custom Validator
// src/{name}.application/Validators/EmailValidator.cs
using FluentValidation;
using FluentValidation.Validators;
namespace {name}.application.validators;
/// <summary>
/// Validates email format with stricter rules than built-in EmailAddress
/// </summary>
public sealed class StrictEmailValidator<T> : PropertyValidator<T, string>
{
public override string Name => "StrictEmailValidator";
public override bool IsValid(ValidationContext<T> context, string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return true; // Let NotEmpty handle null/empty
}
//