一键导入
fluent-validation
Use when adding FluentValidation validators, custom rules, or async validation to a .NET project.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use when adding FluentValidation validators, custom rules, or async validation to a .NET project.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Use when adding caching to .NET APIs or optimizing response times with distributed cache, output cache, or ETags.
Use when configuring API response formats, custom formatters, or Accept header handling.
Use when building controller-based REST APIs with action results, model binding, or MediatR integration.
Use when creating RESTful API controllers with MediatR dispatch and ProblemDetails error responses.
Use when designing gRPC services, proto files, or adding gRPC-Web or JSON transcoding.
Use when building minimal API endpoints with route groups, filters, or TypedResults.
| name | fluent-validation |
| description | Use when adding FluentValidation validators, custom rules, or async validation to a .NET project. |
| metadata | {"category":"core","agent":"dotnet-architect","when-to-use":"When writing input validation rules using FluentValidation"} |
Include() and composition, not inheritance hierarchiesCreateOrderRequest, UpdateCustomerCommandIValidateOptions<T>required keyword or ArgumentNullException.ThrowIfNull sufficesusing FluentValidation;
namespace Ordering.Application.Validators;
public sealed class CreateOrderRequestValidator : AbstractValidator<CreateOrderRequest>
{
public CreateOrderRequestValidator()
{
RuleFor(x => x.CustomerId)
.NotEmpty()
.WithMessage("Customer ID is required.");
RuleFor(x => x.OrderDate)
.LessThanOrEqualTo(DateOnly.FromDateTime(DateTime.UtcNow))
.WithMessage("Order date cannot be in the future.");
RuleFor(x => x.LineItems)
.NotEmpty()
.WithMessage("Order must contain at least one line item.");
RuleForEach(x => x.LineItems)
.SetValidator(new LineItemValidator());
}
}
public sealed class LineItemValidator : AbstractValidator<LineItemDto>
{
public LineItemValidator()
{
RuleFor(x => x.ProductId).NotEmpty();
RuleFor(x => x.Quantity).GreaterThan(0);
RuleFor(x => x.UnitPrice).GreaterThan(0m);
}
}
Register all validators from an assembly in one call:
using FluentValidation;
// In Program.cs or a DI module
builder.Services.AddValidatorsFromAssemblyContaining<CreateOrderRequestValidator>();
This scans the assembly and registers every IValidator<T> as Scoped by default. Override with:
builder.Services.AddValidatorsFromAssemblyContaining<CreateOrderRequestValidator>(
lifetime: ServiceLifetime.Transient);
Inject IValidator<T> and call ValidateAsync explicitly:
using FluentValidation;
namespace Ordering.Application.Handlers;
public sealed class CreateOrderHandler(
IValidator<CreateOrderRequest> validator,
IOrderRepository repository)
{
public async Task<Guid> HandleAsync(
CreateOrderRequest request,
CancellationToken ct = default)
{
var result = await validator.ValidateAsync(request, ct);
if (!result.IsValid)
{
throw new ValidationException(result.Errors);
}
var order = Order.Create(request.CustomerId, request.OrderDate, request.LineItems);
await repository.AddAsync(order, ct);
return order.Id;
}
}
Use an endpoint filter to validate before the handler runs:
using FluentValidation;
namespace Ordering.Api.Filters;
public sealed class ValidationFilter<T> : IEndpointFilter where T : class
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
var validator = context.HttpContext.RequestServices.GetService<IValidator<T>>();
if (validator is null)
{
return await next(context);
}
var model = context.Arguments.OfType<T>().FirstOrDefault();
if (model is null)
{
return await next(context);
}
var result = await validator.ValidateAsync(model);
if (!result.IsValid)
{
return Results.ValidationProblem(
result.ToDictionary(),
title: "Validation Failed",
type: "https://tools.ietf.org/html/rfc9110#section-15.5.1");
}
return await next(context);
}
}
Register the filter on an endpoint or group:
app.MapPost("/api/orders", CreateOrder)
.AddEndpointFilter<ValidationFilter<CreateOrderRequest>>();
// Or apply to an entire route group
app.MapGroup("/api/orders")
.AddEndpointFilter<ValidationFilter<CreateOrderRequest>>();
Use an action filter that resolves IValidator<> for each action argument:
using FluentValidation;
using FluentValidation.AspNetCore;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace Ordering.Api.Filters;
public sealed class FluentValidationActionFilter(IServiceProvider serviceProvider)
: IAsyncActionFilter
{
public async Task OnActionExecutionAsync(
ActionExecutingContext context, ActionExecutionDelegate next)
{
foreach (var (_, value) in context.ActionArguments)
{
if (value is null) continue;
var validatorType = typeof(IValidator<>).MakeGenericType(value.GetType());
if (serviceProvider.GetService(validatorType) is not IValidator validator)
continue;
var result = await validator.ValidateAsync(new ValidationContext<object>(value));
if (!result.IsValid)
result.AddToModelState(context.ModelState);
}
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(
new ValidationProblemDetails(context.ModelState));
return;
}
await next();
}
}
Register globally in Program.cs:
builder.Services.AddControllers(o => o.Filters.Add<FluentValidationActionFilter>());
Use Must() for synchronous custom rules and cross-field validation:
public sealed class DateRangeRequestValidator : AbstractValidator<DateRangeRequest>
{
public DateRangeRequestValidator()
{
RuleFor(x => x.StartDate)
.NotEmpty();
RuleFor(x => x.EndDate)
.NotEmpty()
.Must((request, endDate) => endDate > request.StartDate)
.WithMessage("End date must be after start date.");
RuleFor(x => x.Currency)
.Must(BeAValidCurrencyCode)
.WithMessage("Currency must be a valid ISO 4217 code.");
}
private static bool BeAValidCurrencyCode(string currency) =>
currency.Length == 3 && currency.All(char.IsUpper);
}
Use MustAsync() when validation requires I/O such as database uniqueness checks:
public sealed class CreateCustomerRequestValidator : AbstractValidator<CreateCustomerRequest>
{
public CreateCustomerRequestValidator(ICustomerRepository repository)
{
RuleFor(x => x.Email)
.NotEmpty().EmailAddress()
.MustAsync(async (email, ct) => !await repository.ExistsByEmailAsync(email, ct))
.WithMessage("A customer with this email already exists.");
RuleFor(x => x.TaxId)
.NotEmpty()
.MustAsync(async (taxId, ct) => !await repository.ExistsByTaxIdAsync(taxId, ct))
.WithMessage("A customer with this Tax ID already exists.");
}
}
Important: async validators require ValidateAsync() -- calling Validate() synchronously will throw.
Extract shared validation logic with Include():
public sealed class PaginationValidator : AbstractValidator<IPaginatedRequest>
{
public PaginationValidator()
{
RuleFor(x => x.Page).GreaterThanOrEqualTo(1);
RuleFor(x => x.PageSize).InclusiveBetween(1, 100);
}
}
public sealed class AuditFieldsValidator : AbstractValidator<IAuditableRequest>
{
public AuditFieldsValidator()
{
RuleFor(x => x.CorrelationId).NotEmpty();
RuleFor(x => x.RequestedBy).NotEmpty().MaximumLength(256);
}
}
public sealed class SearchOrdersRequestValidator : AbstractValidator<SearchOrdersRequest>
{
public SearchOrdersRequestValidator()
{
Include(new PaginationValidator());
Include(new AuditFieldsValidator());
RuleFor(x => x.Status)
.IsInEnum()
.When(x => x.Status.HasValue);
}
}
Map FluentValidation errors to RFC 9457 ProblemDetails via IExceptionHandler:
using FluentValidation;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
namespace Ordering.Api.ExceptionHandlers;
public sealed class ValidationExceptionHandler : IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext, Exception exception, CancellationToken ct)
{
if (exception is not ValidationException validationException)
return false;
var errors = validationException.Errors
.GroupBy(e => e.PropertyName)
.ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage).ToArray());
var problemDetails = new ValidationProblemDetails(errors)
{
Type = "https://tools.ietf.org/html/rfc9110#section-15.5.1",
Title = "Validation Failed",
Status = StatusCodes.Status400BadRequest,
Instance = httpContext.Request.Path
};
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
await httpContext.Response.WriteAsJsonAsync(problemDetails, ct);
return true;
}
}
Register in Program.cs:
builder.Services.AddExceptionHandler<ValidationExceptionHandler>();
builder.Services.AddProblemDetails();
app.UseExceptionHandler();
| Scenario | Approach | Why |
|---|---|---|
| Simple DTO field constraints | RuleFor with built-in validators | Readable, testable, no custom logic needed |
| Cross-field validation | Must() with model access | Access to sibling properties via (model, field) overload |
| Uniqueness / DB lookup | MustAsync() with injected repo | Requires async I/O — keeps validator DI-friendly |
| Shared pagination / audit rules | Include() with interface validators | Avoids duplication across multiple request validators |
| Minimal API validation | ValidationFilter<T> endpoint filter | Runs before handler, returns ValidationProblem automatically |
| MVC controller validation | FluentValidationActionFilter | Integrates with ModelState and ValidationProblemDetails |
| Complex nested objects | SetValidator() on child + RuleForEach | Composes child validators, validates each collection element |
| Domain invariants | Do NOT use FluentValidation | Enforce inside entity constructor / methods |
| Startup config validation | IValidateOptions<T> with FluentValidation | Fail fast at app start if configuration is invalid |
| Anti-Pattern | Correct Approach |
|---|---|
| Validating domain entities with FluentValidation | Domain entities enforce their own invariants in constructors and methods |
Calling Validate() synchronously when async rules exist | Always use ValidateAsync() — sync call throws on async rules |
| One mega-validator for all request types | One validator per request model, compose with Include() |
| Throwing generic exceptions on validation failure | Throw ValidationException(result.Errors) and handle with IExceptionHandler |
| Duplicating rules across validators | Extract shared rules into reusable validators and use Include() |
| Validating inside the controller action body | Use filters or middleware to validate before the action executes |
Ignoring CancellationToken in async validators | Pass CancellationToken through MustAsync to support request cancellation |
| Hard-coding error messages without context | Use WithMessage() with placeholders: {PropertyName}, {PropertyValue} |
| Registering validators as Singleton when they have scoped dependencies | Use default Scoped lifetime or Transient if injecting scoped services |
AbstractValidator<T> implementations in Application/Validators/Program.cs for AddValidatorsFromAssembly registration callsIExceptionHandler implementations that handle ValidationException{RequestType}Validator