Валидация: DataAnnotations и FluentValidation
↑ 3.3 Web API: контроллеры, Minimal API, валидация · ← Предыдущая · → Следующая
Зачем это на собесе
Валидацию входа проверяют почти на каждом собеседовании: где она, какой формат ответа и где не хватает атрибутов.
Объяснение
DataAnnotations
public record CreateOrderRequest(
[property: Required, StringLength(100)] string Customer,
[property: Range(1, 1000)] int Quantity,
[property: EmailAddress] string Email);С [ApiController] невалидная модель автоматически даёт 400 с ValidationProblemDetails. Атрибуты просты, но плохо выражают сложные и контекстные правила.
FluentValidation
public class CreateOrderValidator : AbstractValidator<CreateOrderRequest>
{
public CreateOrderValidator(IProductCatalog catalog)
{
RuleFor(x => x.Customer).NotEmpty().MaximumLength(100);
RuleFor(x => x.Quantity).InclusiveBetween(1, 1000);
RuleFor(x => x.Email).EmailAddress();
RuleFor(x => x.ProductId).MustAsync(catalog.ExistsAsync).WithMessage("Товар не найден");
}
}
builder.Services.AddValidatorsFromAssemblyContaining<CreateOrderValidator>();| Критерий | DataAnnotations | FluentValidation |
|---|---|---|
| Где правила | атрибуты в модели | отдельный класс |
| Условные и зависимые правила | ограниченно | When, Must, DependentRules |
| Асинхронные проверки, DI | нет | да |
| Локализация | ресурсы | ресурсы + гибкость |
В Minimal API
orders.MapPost("/", async (CreateOrderRequest req, IValidator<CreateOrderRequest> v, IOrderService s) =>
{
var result = await v.ValidateAsync(req);
if (!result.IsValid) return Results.ValidationProblem(result.ToDictionary());
return Results.Created(...);
});Можно вынести в endpoint filter (см. Фильтры: action, exception, resource, endpoint filters).
Нюансы и подводные камни
- Валидация формы (формат, диапазоны) ≠ бизнес-инварианты: вторые проверяются в доменной логике.
- Не доверяйте клиентской валидации.
- Не возвращайте детали, раскрывающие внутренности (SQL, стек).
- Проверки уникальности через валидатор не заменяют unique-индекс: возможны гонки.
- Nullable reference types не заменяют
Requiredдля JSON:nullможет прийти.
Практика
- Реализуйте валидатор с зависимыми правилами и локализацией.
- Сделайте generic endpoint filter, вызывающий
IValidator<T>. - Убедитесь, что формат ошибок единый (
ValidationProblemDetails).
Вопросы с ответами
Где должна быть валидация?
На границе (форма входных данных) и в домене (инварианты). На клиенте — только для UX.
Когда FluentValidation лучше DataAnnotations?
Когда правила сложные, условные, асинхронные или требуют DI.
Заменяет ли валидатор unique-индекс?
Нет, между проверкой и записью возможна гонка; ограничение должно быть в БД.