agentleFS
landim32landim32/awesome-ai-skills/skills/dotnet-fluent-validation/SKILL.md

dotnet-fluent-validation

Skill1 starsChanged 7 months agoMITView on GitHub

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.

2 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

3 rules. About 0 are common, 1 is distinctive, and 2 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Add FluentValidation to the project where validators will live (typically the Domain/Business layer, same as services):

    line 39
  2. Very specific:

    Create a Validators/ folder in the project where services live.

    line 50
  3. Very specific:

    If using FluentValidation's built-in ValidateAndThrow (which throws ValidationException), catch it in controllers or use a global exception …

    line 304

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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:

  1. Read the project structure — Identify the projects/layers in the solution (API, Domain/Business, DTO, Infra, etc.)
  2. Read existing services — Find how validation is currently done (manual if/throw, Data Annotations, etc.)
  3. Read existing DI setup — Find where services are registered (Program.cs, Startup.cs, Initializer.cs, etc.)
  4. 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

TypeWhereExample
Input/FormatFluentValidation (AbstractValidator)Empty fields, email format, min/max length, numeric range, enum values
Business RulesService layerDuplicate name/email (requires DB), access control, cross-entity rules
Cross-fieldFluentValidation with When/Must"EndDate must be after StartDate", conditional required fields
Async/DB rulesFluentValidation with MustAsync or service layerUniqueness checks — prefer service unless reused across multiple services

Checklist

#ActionDescription
1Installdotnet add package FluentValidation on the project with services
2Installdotnet add package FluentValidation.DependencyInjectionExtensions
3CreateValidators/{DtoName}Validator.cs — one per DTO
4RegisterAdd services.AddValidatorsFromAssemblyContaining<...>() in DI setup
5IntegrateInject IValidator<TDto> in services, call ValidateAndThrow()
6Handle errorsCatch ValidationException in controllers or add global exception handler
7OptionalCreate ValidationFilter for automatic pipeline validation
8OptionalCreate validators with DI for business rules with DB access

Response Guidelines

  1. Read the project first — Understand the structure, naming, namespaces, and DI setup before writing code
  2. Follow project conventions — Match existing code style, namespace patterns, and folder structure
  3. Match existing error messages — Reuse the exact same error strings to avoid breaking frontend/clients
  4. Keep business rules in services — Only move input/format validations to FluentValidation
  5. One validator per DTO — Don't combine validators for different DTOs
  6. Adapt error responses — Match the project's existing error response contract
  7. Follow the order — Package → Validator → DI → Service integration → Error handling

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.