Валидация данных

Валидация входных данных: data annotations, ModelState и автоматические 400.

Суть: валидация проверяет, что присланные данные корректны (email — это email, имя не пустое, возраст в диапазоне) до того, как они попадут в бизнес-логику. В ASP.NET Core это делают атрибуты-аннотации, а [ApiController] автоматически отклоняет невалидные запросы с кодом 400.

Никогда не доверяйте клиенту. Любой запрос может прийти с пустыми, слишком длинными или бессмысленными данными — случайно или со злым умыслом. Валидация — первая линия обороны бэкенда.

Аннотации на DTO

public class CreateUserDto
{
    [Required]
    [StringLength(50, MinimumLength = 2)]
    public string Name { get; set; } = "";

    [Required]
    [EmailAddress]
    public string Email { get; set; } = "";

    [Range(18, 120)]
    public int Age { get; set; }
}

С [ApiController] вам даже не нужно проверять вручную: если DTO невалиден, фреймворк сам вернёт 400 с описанием ошибок по полям, не заходя в метод.

Поток валидации

POST /api/users  { "name": "", "age": 5 }
        |
        v
[Model binding] заполняет DTO
        |
        v
[Валидация по аннотациям]
        |
   ошибки? --да--> 400 + список ошибок (метод не вызван)
        |
        нет
        v
[Ваш метод обрабатывает валидный DTO]

Как работает под капотом

После биндинга фреймворк прогоняет каждое свойство DTO через его атрибуты-валидаторы. Каждый атрибут (Required, Range) умеет проверить значение и при провале добавить сообщение в ModelState. Если ModelState.IsValid равно false и стоит [ApiController], срабатывает фильтр, который формирует ответ ValidationProblemDetails (400) и прерывает выполнение. Логику валидации можно расширять кастомными атрибутами или интерфейсом IValidatableObject.

# Аналог валидации DTO по правилам-аннотациям
import re

def validate(dto):
    errors = {}
    name = dto.get("name", "")
    if not (2 <= len(name) <= 50):
        errors["name"] = "длина 2..50"
    email = dto.get("email", "")
    if not re.match(r"^[^@]+@[^@]+\.[^@]+$", email):
        errors["email"] = "невалидный email"
    age = dto.get("age", 0)
    if not (18 <= age <= 120):
        errors["age"] = "возраст 18..120"
    return errors

errs = validate({"name": "", "email": "bad", "age": 5})
if errs:
    print("400 Bad Request:", errs)   # метод не выполнится
else:
    print("OK, обрабатываем")

Попробуй сам ▶ — функция собирает ошибки по полям и «возвращает 400», как делает [ApiController]. Подставьте валидные данные — увидите «OK».

Частые ошибки

  • Проверять данные только на фронтенде. Фронт можно обойти — бэкенд обязан валидировать сам.
  • Валидировать вручную при наличии [ApiController]. Дублирование: фреймворк уже вернёт 400 за вас.
  • Бизнес-правила в аннотациях. «email уникален в БД» — это не аннотация, а проверка в сервисе/БД.

Best practices

  • Простые правила (формат, длина, диапазон) — аннотациями на DTO.
  • Сложные/доменные правила — в слое сервиса, с понятными ответами 400/409.
  • Для богатой валидации рассмотрите FluentValidation — отдельные классы-правила, удобно тестировать.

Три уровня проверки данных

Полезно разделять валидацию по уровням. Синтаксическая (формат) — это data annotations: [Required], [EmailAddress], [StringLength], [Range], [RegularExpression]. Они проверяют форму данных и работают автоматически с [ApiController]. Семантическая (бизнес-правила одной модели) — реализуется через IValidatableObject или кастомные атрибуты: например «дата окончания позже даты начала». Доменная (правила, требующие БД или контекста) — живёт в сервисе: «email уникален», «на складе достаточно товара».

Смешивать уровни вредно. Доменную проверку нельзя засунуть в аннотацию — у атрибута нет доступа к БД и он сработает не вовремя. И наоборот, формат не стоит проверять руками в сервисе, дублируя то, что фреймворк делает сам. Чёткое распределение делает код понятным: по DTO видно формат, по сервису — бизнес-правила.

FluentValidation и сообщения об ошибках

Когда правил много или они сложные, аннотации становятся тесными. Тогда берут FluentValidation: для каждого DTO пишут отдельный класс-валидатор с цепочками правил (RuleFor(x => x.Email).NotEmpty().EmailAddress()). Плюсы — правила вынесены из модели, легко тестируются и читаются, поддерживают условную логику и асинхронные проверки. Это популярный выбор в средних и крупных проектах.

Отдельная тема — качество сообщений об ошибках. Клиенту (и человеку, и фронтенду) нужны понятные, привязанные к полю сообщения: не «invalid input», а «email: некорректный формат». Формат ProblemDetails с картой ошибок по полям, который ASP.NET Core возвращает автоматически, как раз решает это. Хорошая валидация — не только «отклонить плохое», но и «понятно объяснить, что именно не так».

Итог: валидация защищает бэкенд от мусорных данных, а [ApiController] автоматизирует возврат 400. Дальше — как документировать API, чтобы им было удобно пользоваться.

Проверьте себя
1. Что произойдёт при невалидном DTO, если стоит [ApiController]?
AМетод выполнится с пустыми данными
BФреймворк сам вернёт 400 с ошибками по полям, не заходя в метод
CБудет 500
DЗапрос проигнорируется молча
2. Где проверять правило «email уникален в базе»?
AАннотацией [Unique] на DTO
BВ слое сервиса/БД, а не аннотацией формата
CНа фронтенде и только там
DЭто делает model binding