Dotnet fluent validation
Skill landim32/awesome-ai-skills/skills/dotnet-fluent-validation
Claude AI Skills repository
npx -y skills add landim32/awesome-ai-skills --skill dotnet-fluent-validationAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Guides the implementation of FluentValidation validators for DTOs in a .NET 8 project. Creates validator classes, registers them in DI, integrates with services, and configures the validation pipeline. Use when adding input validation, creating validators, or replacing manual if/throw validation with FluentValidation.
SKILL.md
12.9 KB, as published. Nobody here has run it
FluentValidation — Implementation Guide
You are an expert assistant that helps developers implement FluentValidation in .NET 8 projects. You guide the user through creating validators, registering them in DI, and integrating with the existing codebase.
Input
The user will describe the DTO or entity to validate: $ARGUMENTS
Before generating code:
- Read the project structure — Identify the projects/layers in the solution (API, Domain/Business, DTO, Infra, etc.)
- Read existing services — Find how validation is currently done (manual if/throw, Data Annotations, etc.)
- Read existing DI setup — Find where services are registered (Program.cs, Startup.cs, Initializer.cs, etc.)
- Match existing patterns — Follow the project's naming conventions, namespaces, and code style
Architecture & Data Flow
Controller → Service → Validator → (if invalid) throw ValidationException
→ (if valid) proceed with business logic
Typical current pattern: Services validate inline with if/throw new Exception("message").
New pattern: Dedicated AbstractValidator<TDto> classes, called in services before business logic.
Step-by-Step Implementation
Step 1: Install NuGet Packages
Add FluentValidation to the project where validators will live (typically the Domain/Business layer, same as services):
dotnet add {ProjectName}.csproj package FluentValidation
dotnet add {ProjectName}.csproj package FluentValidation.DependencyInjectionExtensions
Step 2: Create Validator — Validators/{DtoName}Validator.cs
Create a Validators/ folder in the project where services live.
Naming convention: {DtoName}Validator.cs — one validator per DTO.
Example — CreateUserDtoValidator:
using FluentValidation;
namespace {ProjectNamespace}.Validators
{
public class CreateUserDtoValidator : AbstractValidator<CreateUserDto>
{
public CreateUserDtoValidator()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Name is required")
.MaximumLength(200).WithMessage("Name must not exceed 200 characters");
RuleFor(x => x.Email)
.NotEmpty().WithMessage("Email is required")
.EmailAddress().WithMessage("Email is not valid");
RuleFor(x => x.Age)
.GreaterThan(0).WithMessage("Age must be greater than 0")
.LessThanOrEqualTo(150).WithMessage("Age is invalid");
}
}
}
Common Validation Rules Reference
// String
RuleFor(x => x.Name).NotEmpty().WithMessage("Name is required");
RuleFor(x => x.Name).MaximumLength(200).WithMessage("Name is too long");
RuleFor(x => x.Name).MinimumLength(3).WithMessage("Name is too short");
RuleFor(x => x.Name).Matches(@"^[a-zA-Z\s]+$").WithMessage("Name contains invalid characters");
// Email
RuleFor(x => x.Email).NotEmpty().EmailAddress().WithMessage("Email is not valid");
// Numeric
RuleFor(x => x.Price).GreaterThan(0).WithMessage("Price must be greater than 0");
RuleFor(x => x.Percentage).InclusiveBetween(0, 100).WithMessage("Must be between 0 and 100");
// Enum
RuleFor(x => x.Status).IsInEnum().WithMessage("Invalid status");
// Date
RuleFor(x => x.BirthDate).LessThan(DateTime.Now).WithMessage("Birth date must be in the past");
// Conditional
RuleFor(x => x.Slug)
.NotEmpty().WithMessage("Slug is required")
.When(x => x.Id > 0); // Only required on update
// Nested object
RuleFor(x => x.Address).SetValidator(new AddressValidator());
// Collection
RuleForEach(x => x.Items).SetValidator(new OrderItemValidator());
// Custom rule
RuleFor(x => x.Document)
.Must(doc => IsValidCpf(doc)).WithMessage("Invalid CPF")
.When(x => !string.IsNullOrEmpty(x.Document));
Step 3: Register Validators in DI
Find the project's DI registration file (e.g., Program.cs, Startup.cs, or a custom Initializer.cs).
Option A — Auto-register all validators (recommended):
using FluentValidation;
// Scans the assembly and registers all AbstractValidator<T> implementations
services.AddValidatorsFromAssemblyContaining<CreateUserDtoValidator>(ServiceLifetime.Scoped);
Option B — Register individually:
using FluentValidation;
services.AddScoped<IValidator<CreateUserDto>, CreateUserDtoValidator>();
services.AddScoped<IValidator<UpdateUserDto>, UpdateUserDtoValidator>();
Note: Match the service lifetime used by the project (Scoped, Transient, etc.).
Step 4: Inject and Use in Services
Inject IValidator<TDto> into the service and call it before the business logic.
Before (manual validation):
public UserModel Insert(CreateUserDto dto)
{
if (string.IsNullOrEmpty(dto.Name))
throw new Exception("Name is required");
if (string.IsNullOrEmpty(dto.Email))
throw new Exception("Email is required");
// ... more manual checks
// business logic
}
After (FluentValidation):
public class UserService : IUserService
{
private readonly IValidator<CreateUserDto> _createValidator;
// ... other dependencies
public UserService(IValidator<CreateUserDto> createValidator /* ... */)
{
_createValidator = createValidator;
}
public UserModel Insert(CreateUserDto dto)
{
// FluentValidation — replaces manual if/throw checks
_createValidator.ValidateAndThrow(dto);
// Business rules that require database (keep in service)
var existing = _repository.GetByEmail(dto.Email);
if (existing != null)
throw new Exception("Email already registered");
// ... proceed with insert
}
}
Important distinction:
- Input/format validations → Move to FluentValidation (empty, email format, range, length)
- Business rules with DB access → Keep in the service (duplicate checks, authorization, cross-entity rules)
Step 5 (Optional): Validation Filter for Automatic Pipeline Validation
Automatically validate request DTOs before the controller action runs, returning 400 Bad Request on failure.
Create Filters/ValidationFilter.cs in the API project:
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace {ApiNamespace}.Filters
{
public class ValidationFilter : IAsyncActionFilter
{
private readonly IServiceProvider _serviceProvider;
public ValidationFilter(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
foreach (var argument in context.ActionArguments.Values)
{
if (argument == null) continue;
var validatorType = typeof(IValidator<>).MakeGenericType(argument.GetType());
var validator = _serviceProvider.GetService(validatorType) as IValidator;
if (validator == null) continue;
var validationContext = new ValidationContext<object>(argument);
var result = await validator.ValidateAsync(validationContext);
if (!result.IsValid)
{
var errors = result.Errors.Select(e => e.ErrorMessage).ToList();
context.Result = new BadRequestObjectResult(new
{
success = false,
message = "Validation failed",
errors
});
return;
}
}
await next();
}
}
}
Register the filter:
builder.Services.AddControllers(options =>
{
options.Filters.Add<ValidationFilter>();
});
Adapt the error response to match the project's existing error response contract (e.g.,
StatusResult,ApiResponse, or a custom error DTO).
Step 6 (Optional): Validator with Dependency Injection
For validators that need access to repositories or external services (e.g., uniqueness checks):
using FluentValidation;
namespace {ProjectNamespace}.Validators
{
public class CreateUserBusinessValidator : AbstractValidator<CreateUserDto>
{
public CreateUserBusinessValidator(IUserRepository userRepository)
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Name is required");
RuleFor(x => x.Email)
.NotEmpty().WithMessage("Email is required")
.EmailAddress().WithMessage("Email is not valid")
.MustAsync(async (email, cancellation) =>
{
var existing = await userRepository.GetByEmailAsync(email);
return existing == null;
}).WithMessage("Email already registered");
}
}
}
Use with caution: Validators with DB access are heavier and harder to unit test in isolation. Prefer keeping database-dependent checks in the service layer.
Step 7 (Optional): Handling ValidationException in Controllers
If using FluentValidation's built-in ValidateAndThrow (which throws ValidationException), catch it in controllers or use a global exception handler:
Per-controller approach:
[HttpPost]
public IActionResult Create([FromBody] CreateUserDto dto)
{
try
{
var result = _service.Insert(dto);
return Ok(result);
}
catch (ValidationException ex)
{
var errors = ex.Errors.Select(e => e.ErrorMessage).ToList();
return BadRequest(new { success = false, errors });
}
catch (Exception ex)
{
return StatusCode(500, ex.Message);
}
}
Global exception middleware approach:
// In Program.cs or a middleware class:
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;
if (exception is ValidationException validationEx)
{
context.Response.StatusCode = 400;
var errors = validationEx.Errors.Select(e => e.ErrorMessage).ToList();
await context.Response.WriteAsJsonAsync(new { success = false, errors });
}
else
{
context.Response.StatusCode = 500;
await context.Response.WriteAsJsonAsync(new { success = false, message = exception?.Message });
}
});
});
Validation Separation Guide
| Type | Where | Example |
|---|---|---|
| Input/Format | FluentValidation (AbstractValidator) | Empty fields, email format, min/max length, numeric range, enum values |
| Business Rules | Service layer | Duplicate name/email (requires DB), access control, cross-entity rules |
| Cross-field | FluentValidation with When/Must | "EndDate must be after StartDate", conditional required fields |
| Async/DB rules | FluentValidation with MustAsync or service layer | Uniqueness checks — prefer service unless reused across multiple services |
Checklist
| # | Action | Description |
|---|---|---|
| 1 | Install | dotnet add package FluentValidation on the project with services |
| 2 | Install | dotnet add package FluentValidation.DependencyInjectionExtensions |
| 3 | Create | Validators/{DtoName}Validator.cs — one per DTO |
| 4 | Register | Add services.AddValidatorsFromAssemblyContaining<...>() in DI setup |
| 5 | Integrate | Inject IValidator<TDto> in services, call ValidateAndThrow() |
| 6 | Handle errors | Catch ValidationException in controllers or add global exception handler |
| 7 | Optional | Create ValidationFilter for automatic pipeline validation |
| 8 | Optional | Create validators with DI for business rules with DB access |
Response Guidelines
- Read the project first — Understand the structure, naming, namespaces, and DI setup before writing code
- Follow project conventions — Match existing code style, namespace patterns, and folder structure
- Match existing error messages — Reuse the exact same error strings to avoid breaking frontend/clients
- Keep business rules in services — Only move input/format validations to FluentValidation
- One validator per DTO — Don't combine validators for different DTOs
- Adapt error responses — Match the project's existing error response contract
- Follow the order — Package → Validator → DI → Service integration → Error handling