Ответ
Да, постоянно. Валидация — критически важный слой для безопасности и целостности данных. В 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" — "неверный формат". А всю техническую подноготную оставляю для логов, себе под одеяло.