Приходилось ли делать валидацию данных в ASP.NET Core?

Ответ

Да, постоянно. Валидация — критически важный слой для безопасности и целостности данных. В ASP.NET Core применяю многоуровневый подход.

1. Валидация на уровне модели (Model Validation)

Использую атрибуты из пространства имён System.ComponentModel.DataAnnotations для декларативного описания правил.

public class UserRegistrationDto
{
    [Required(ErrorMessage = "Имя пользователя обязательно.")]
    [StringLength(50, MinimumLength = 3)]
    public string Username { get; set; }

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

    [Required]
    [DataType(DataType.Password)]
    [RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*d).{8,}$", 
        ErrorMessage = "Пароль должен содержать минимум 8 символов, заглавную и строчную буквы, цифру.")]
    public string Password { get; set; }

    [DataType(DataType.Password)]
    [Compare(nameof(Password), ErrorMessage = "Пароли не совпадают.")]
    public string ConfirmPassword { get; set; }

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

В контроллере проверка автоматическая:

[HttpPost]
public IActionResult Register(UserRegistrationDto model)
{
    if (!ModelState.IsValid) // Автоматическая проверка атрибутов
    {
        // Возвращаем ошибки клиенту (вместе с состоянием модели для формы)
        return BadRequest(ModelState);
    }
    // Логика регистрации...
}

2. Сложная бизнес-логика с FluentValidation

Для правил, зависящих от контекста или нескольких полей, предпочитаю библиотеку FluentValidation.

public class UserRegistrationValidator : AbstractValidator<UserRegistrationDto>
{
    public UserRegistrationValidator(IUserRepository repository)
    {
        RuleFor(x => x.Username)
            .MustAsync(async (name, cancellation) => 
            {
                return !await repository.UserExistsAsync(name);
            })
            .WithMessage("Имя пользователя уже занято."); // Кастомная асинхронная проверка

        RuleFor(x => x.Age)
            .Must(age => age >= 18).When(x => x.Email.EndsWith(".ru"))
            .WithMessage("Для российских пользователей возраст должен быть 18+"); // Условная валидация
    }
}
// Регистрация валидатора в DI-контейнере
services.AddScoped<IValidator<UserRegistrationDto>, UserRegistrationValidator>();

3. Валидация в сервисном слое

Атрибуты не покрывают всю бизнес-логику. Критичные проверки дублируются в сервисах.

public class UserService : IUserService
{
    public async Task<RegistrationResult> RegisterUserAsync(UserRegistrationDto dto)
    {
        // Повторная проверка "на всякий случай"
        if (dto == null) throw new ArgumentNullException(nameof(dto));

        // Сложная бизнес-логика
        if (await _repository.IsEmailBannedAsync(dto.Email))
        {
            return RegistrationResult.Failed("Email находится в чёрном списке.");
        }
        // ...
    }
}

Важно: Всегда возвращаю клиенту понятные сообщения об ошибках (желательно на его языке) в структурированном формате (например, JSON с полем errors), но никогда не раскрываю внутренние детали реализации или стек-трейсы в продакшене.

Ответ 18+ 🔞

Да ты что, серьёзно спрашиваешь? Ну конечно, постоянно! Валидация — это ж как бронежилет для твоего приложения, без неё на прод выходить — это самоубийство, блядь. В ASP.NET Core я её выстраиваю в несколько слоёв, чтобы наверняка.

1. Валидация на уровне модели — основа основ

Тут всё просто, как три копейки: навешиваю атрибуты из System.ComponentModel.DataAnnotations. Чтоб даже дурак понял, что от него хотят.

public class UserRegistrationDto
{
    [Required(ErrorMessage = "Имя пользователя обязательно.")]
    [StringLength(50, MinimumLength = 3)]
    public string Username { get; set; }

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

    [Required]
    [DataType(DataType.Password)]
    [RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*d).{8,}$", 
        ErrorMessage = "Пароль должен содержать минимум 8 символов, заглавную и строчную буквы, цифру.")]
    public string Password { get; set; }

    [DataType(DataType.Password)]
    [Compare(nameof(Password), ErrorMessage = "Пароли не совпадают.")]
    public string ConfirmPassword { get; set; }

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

А в контроллере за меня уже всё работает, красота:

[HttpPost]
public IActionResult Register(UserRegistrationDto model)
{
    if (!ModelState.IsValid) // Фреймворк сам всё проверил
    {
        // Отправляю ошибки обратно, пусть знает, где накосячил
        return BadRequest(ModelState);
    }
    // Логика регистрации...
}

2. FluentValidation для сложной ерунды

А вот когда правила такие, что голова кругом — проверка уникальности имени в базе или там зависимость полей — тут атрибуты пасуют. На помощь приходит FluentValidation, просто бомба библиотека, а не инструмент.

public class UserRegistrationValidator : AbstractValidator<UserRegistrationDto>
{
    public UserRegistrationValidator(IUserRepository repository)
    {
        RuleFor(x => x.Username)
            .MustAsync(async (name, cancellation) => 
            {
                return !await repository.UserExistsAsync(name);
            })
            .WithMessage("Имя пользователя уже занято."); // Асинхронно в базу сходил, всё проверил

        RuleFor(x => x.Age)
            .Must(age => age >= 18).When(x => x.Email.EndsWith(".ru"))
            .WithMessage("Для российских пользователей возраст должен быть 18+"); // Локальные особенности, ёпта
    }
}
// Не забыть зарегистрировать валидатор в контейнере
services.AddScoped<IValidator<UserRegistrationDto>, UserRegistrationValidator>();

3. Сервисный слой — последний рубеж обороны

Атрибуты и FluentValidation — это хорошо, но я параноик. Поэтому в сервисах, где самая мякотка, я проверяю всё ещё раз. Мало ли что.

public class UserService : IUserService
{
    public async Task<RegistrationResult> RegisterUserAsync(UserRegistrationDto dto)
    {
        // Первым делом — "а объект-то не null?"
        if (dto == null) throw new ArgumentNullException(nameof(dto));

        // А теперь какая-нибудь хитрая бизнес-логика
        if (await _repository.IsEmailBannedAsync(dto.Email))
        {
            return RegistrationResult.Failed("Email находится в чёрном списке.");
        }
        // ...
    }
}

И главное правило, которое я выучил, как Отче наш: клиенту я возвращаю понятные, человеческие сообщения на его языке. Никаких стек-трейсов, внутренних кодов ошибок или фраз вроде "Object reference not set to an instance of an object". В продакшене это просто пиздец какой-то, дверь для злоумышленников. Только структурированный JSON, где ясно видно, что поле "Email" — "неверный формат". А всю техническую подноготную оставляю для логов, себе под одеяло.