.claude/skills/dotnet-fluent-validation/SKILL.md
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.
npx skillsauth add landim32/GitNews dotnet-fluent-validationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
The user will describe the DTO or entity to validate: $ARGUMENTS
Before generating code:
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.
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
Validators/{DtoName}Validator.csCreate 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");
}
}
}
// 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));
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.).
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)
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).
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.
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 });
}
});
});
| 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 |
| # | 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 |
documentation
Generates a comprehensive, standardized README.md for any project. Use when the user wants to create or regenerate a README file following the project's documentation standard.
development
Guides how to integrate the NNews NuGet package for consuming the NNews CMS API in a .NET 8 project. Use when the user wants to consume articles, categories, tags, images, or AI-powered content generation from the NNews API.
data-ai
Creates Mermaid diagrams (.mmd) and generates PNG images from them. Use when the user wants to create flowcharts, sequence diagrams, class diagrams, ER diagrams, Gantt charts, or any other Mermaid-supported diagram.
development
Guides implementation of Clean Architecture in .NET projects. Covers all layers: DTO, Domain (Models, Services, Enums), Infra.Interfaces (Repository, AppServices), Infra (Context, Repository, AppServices), and Application (DI/Startup). Use when creating entities, services, repositories, or restructuring architecture. Works with any project type (Web, Console, Mobile, Windows).